PhysiQuanty commited on
Commit
4be99aa
·
verified ·
1 Parent(s): 9d2ad98
Files changed (1) hide show
  1. app.py +585 -0
app.py ADDED
@@ -0,0 +1,585 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import html
2
+ from datetime import datetime, timezone
3
+
4
+ import gradio as gr
5
+ import numpy as np
6
+ import pandas as pd
7
+ from sentence_transformers import SentenceTransformer
8
+
9
+
10
+ CSV_PATH = "hf_atlas_ai_ml_interests_embedded.csv"
11
+ EMBEDDINGS_PATH = "embeddings_ai_ml_interests.npy"
12
+ MODEL_NAME = "BAAI/bge-small-en-v1.5"
13
+
14
+
15
+ profile_df = pd.read_csv(CSV_PATH)
16
+ profile_embeddings = np.load(EMBEDDINGS_PATH).astype(np.float32)
17
+
18
+ print(f"✅ Loaded {len(profile_df)} HF Atlas profiles")
19
+ print(f"✅ Loaded embeddings: {profile_embeddings.shape}")
20
+
21
+ if len(profile_df) != profile_embeddings.shape[0]:
22
+ raise ValueError(
23
+ f"CSV / embeddings mismatch: {len(profile_df)} rows vs {profile_embeddings.shape[0]} embeddings"
24
+ )
25
+
26
+
27
+ def detect_username_col(df):
28
+ for col in ["user", "username", "namespace"]:
29
+ if col in df.columns:
30
+ return col
31
+ return None
32
+
33
+
34
+ USERNAME_COL = detect_username_col(profile_df)
35
+
36
+ if USERNAME_COL is None:
37
+ raise ValueError("No username column found. Expected one of: user, username, namespace")
38
+
39
+
40
+ def normalize_embeddings_if_needed(embeddings):
41
+ norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
42
+ norms = np.where(norms == 0, 1.0, norms)
43
+ return embeddings / norms
44
+
45
+
46
+ profile_embeddings = normalize_embeddings_if_needed(profile_embeddings)
47
+
48
+ embedder = SentenceTransformer(MODEL_NAME)
49
+
50
+
51
+ def safe_text(value, default=""):
52
+ if value is None:
53
+ return default
54
+
55
+ try:
56
+ if pd.isna(value):
57
+ return default
58
+ except Exception:
59
+ pass
60
+
61
+ text = str(value).strip()
62
+
63
+ if text.lower() in {"nan", "none", "null"}:
64
+ return default
65
+
66
+ return text
67
+
68
+
69
+ def parse_last_seen(value):
70
+ text = safe_text(value)
71
+
72
+ if not text:
73
+ return pd.NaT
74
+
75
+ return pd.to_datetime(text, errors="coerce", utc=True)
76
+
77
+
78
+ def prepare_dates():
79
+ if "last_seen_all_repo" not in profile_df.columns:
80
+ profile_df["_last_seen_dt"] = pd.NaT
81
+ print("⚠️ Column last_seen_all_repo not found. Date filter will only work as no-filter.")
82
+ return
83
+
84
+ profile_df["_last_seen_dt"] = profile_df["last_seen_all_repo"].map(parse_last_seen)
85
+
86
+ known = int(profile_df["_last_seen_dt"].notna().sum())
87
+ unknown = int(profile_df["_last_seen_dt"].isna().sum())
88
+
89
+ print(f"🕒 Known last_seen_all_repo dates: {known}")
90
+ print(f"🕳️ Unknown last_seen_all_repo dates: {unknown}")
91
+
92
+ if known > 0:
93
+ print(f"🕒 Min last_seen: {profile_df['_last_seen_dt'].min()}")
94
+ print(f"🕒 Max last_seen: {profile_df['_last_seen_dt'].max()}")
95
+
96
+
97
+ prepare_dates()
98
+
99
+
100
+ def filter_by_activity(df, activity_filter, custom_days):
101
+ if "_last_seen_dt" not in df.columns:
102
+ return df
103
+
104
+ custom_days = int(custom_days) if custom_days else 0
105
+
106
+ if activity_filter == "No filter":
107
+ return df
108
+
109
+ if activity_filter == "Has known activity date":
110
+ return df[df["_last_seen_dt"].notna()]
111
+
112
+ if activity_filter == "No known activity date":
113
+ return df[df["_last_seen_dt"].isna()]
114
+
115
+ if activity_filter == "Max age in days":
116
+ if custom_days <= 0:
117
+ return df
118
+
119
+ now = pd.Timestamp(datetime.now(timezone.utc))
120
+ cutoff = now - pd.Timedelta(days=custom_days)
121
+
122
+ return df[df["_last_seen_dt"].notna() & (df["_last_seen_dt"] >= cutoff)]
123
+
124
+ return df
125
+
126
+
127
+ def format_number(value):
128
+ text = safe_text(value, "0")
129
+
130
+ try:
131
+ number = int(float(text))
132
+ return f"{number:,}".replace(",", " ")
133
+ except Exception:
134
+ return html.escape(text)
135
+
136
+
137
+ def format_date(value):
138
+ text = safe_text(value)
139
+
140
+ if not text:
141
+ return "unknown"
142
+
143
+ try:
144
+ dt = pd.to_datetime(text, errors="coerce", utc=True)
145
+ if pd.isna(dt):
146
+ return "unknown"
147
+ return dt.strftime("%Y-%m-%d")
148
+ except Exception:
149
+ return html.escape(text)
150
+
151
+
152
+ def truncate(text, max_len=900):
153
+ text = safe_text(text)
154
+
155
+ if len(text) <= max_len:
156
+ return text
157
+
158
+ return text[:max_len].rsplit(" ", 1)[0] + "..."
159
+
160
+
161
+ def get_profile_url(row):
162
+ if "atlas_request_url" in row and safe_text(row["atlas_request_url"]):
163
+ return (
164
+ safe_text(row["atlas_request_url"])
165
+ .replace("/api/users/", "/")
166
+ .replace("/overview", "")
167
+ )
168
+
169
+ username = safe_text(row[USERNAME_COL])
170
+ return f"https://huggingface.co/{username}"
171
+
172
+
173
+ def render_profile_card(row, score, rank):
174
+ username = safe_text(row[USERNAME_COL], "unknown")
175
+ fullname = safe_text(row.get("fullname", ""), "")
176
+ details = safe_text(row.get("details", ""), "")
177
+ ai_ml_interests = truncate(row.get("ai_ml_interests", ""), 800)
178
+ last_seen = format_date(row.get("last_seen_all_repo", ""))
179
+
180
+ num_models = format_number(row.get("numModels", row.get("n_models", 0)))
181
+ num_datasets = format_number(row.get("numDatasets", row.get("n_datasets", 0)))
182
+ num_spaces = format_number(row.get("numSpaces", row.get("n_spaces", 0)))
183
+ followers = format_number(row.get("numFollowers", 0))
184
+ likes = format_number(row.get("numLikes", row.get("numUpvotes", 0)))
185
+
186
+ url = get_profile_url(row)
187
+
188
+ title = html.escape(username)
189
+ fullname_html = html.escape(fullname) if fullname else "—"
190
+ details_html = html.escape(truncate(details, 300)) if details else ""
191
+ interests_html = html.escape(ai_ml_interests).replace("\n", "<br>")
192
+
193
+ extra_details = ""
194
+ if details_html:
195
+ extra_details = f"""
196
+ <div class="details">{details_html}</div>
197
+ """
198
+
199
+ return f"""
200
+ <div class="result-card">
201
+ <div class="result-topline">
202
+ <div>
203
+ <div class="rank">#{rank}</div>
204
+ <a class="username" href="{url}" target="_blank">{title}</a>
205
+ <div class="fullname">{fullname_html}</div>
206
+ </div>
207
+ <div class="score">{score * 100:.2f}%</div>
208
+ </div>
209
+
210
+ {extra_details}
211
+
212
+ <div class="interests">
213
+ <div class="label">AI/ML interests</div>
214
+ <div>{interests_html}</div>
215
+ </div>
216
+
217
+ <div class="stats">
218
+ <span>🧠 Models: <b>{num_models}</b></span>
219
+ <span>📚 Datasets: <b>{num_datasets}</b></span>
220
+ <span>🚀 Spaces: <b>{num_spaces}</b></span>
221
+ <span>❤️ Likes: <b>{likes}</b></span>
222
+ <span>👥 Followers: <b>{followers}</b></span>
223
+ <span>🕒 Last seen: <b>{last_seen}</b></span>
224
+ </div>
225
+ </div>
226
+ """
227
+
228
+
229
+ def build_search_results(query, activity_filter, custom_days, display_count):
230
+ query = safe_text(query)
231
+
232
+ if not query:
233
+ return """
234
+ <div class="empty-state">
235
+ Describe an AI/ML topic, research area, tool, model family, or technical interest.
236
+ </div>
237
+ """, 0, False
238
+
239
+ custom_days = int(custom_days) if custom_days else 0
240
+ display_count = int(display_count)
241
+
242
+ eligible = filter_by_activity(profile_df, activity_filter, custom_days)
243
+
244
+ print("FILTER:", activity_filter, "DAYS:", custom_days, "ELIGIBLE:", len(eligible))
245
+
246
+ if "_last_seen_dt" in eligible.columns and len(eligible) > 0:
247
+ known_eligible = eligible[eligible["_last_seen_dt"].notna()]
248
+ if len(known_eligible) > 0:
249
+ print("ELIGIBLE MIN LAST_SEEN:", known_eligible["_last_seen_dt"].min())
250
+ print("ELIGIBLE MAX LAST_SEEN:", known_eligible["_last_seen_dt"].max())
251
+
252
+ if len(eligible) == 0:
253
+ return """
254
+ <div class="empty-state">
255
+ No profile found for this activity filter.
256
+ </div>
257
+ """, display_count, False
258
+
259
+ eligible_indices = eligible.index.to_numpy()
260
+ eligible_embeddings = profile_embeddings[eligible_indices]
261
+
262
+ query_emb = embedder.encode(
263
+ [query],
264
+ convert_to_numpy=True,
265
+ normalize_embeddings=True,
266
+ ).astype(np.float32)
267
+
268
+ similarities = np.dot(query_emb, eligible_embeddings.T)[0]
269
+
270
+ display_count = max(1, min(display_count, len(eligible_indices)))
271
+ best_local_indices = np.argsort(-similarities)[:display_count]
272
+
273
+ cards = []
274
+
275
+ if activity_filter == "Max age in days" and custom_days > 0:
276
+ filter_label = f"active in last {custom_days} days"
277
+ elif activity_filter == "Has known activity date":
278
+ filter_label = "with known public activity date"
279
+ elif activity_filter == "No known activity date":
280
+ filter_label = "with no known public activity date"
281
+ else:
282
+ filter_label = "no date filter"
283
+
284
+ header = f"""
285
+ <div class="search-summary">
286
+ <b>{len(eligible):,}</b> eligible profiles · showing top <b>{display_count}</b> · <b>{filter_label}</b>
287
+ </div>
288
+ """.replace(",", " ")
289
+
290
+ cards.append(header)
291
+
292
+ for rank, local_idx in enumerate(best_local_indices, start=1):
293
+ global_idx = eligible_indices[local_idx]
294
+ row = profile_df.iloc[global_idx]
295
+ score = float(similarities[local_idx])
296
+ cards.append(render_profile_card(row, score, rank))
297
+
298
+ has_more = display_count < len(eligible_indices)
299
+
300
+ if not has_more:
301
+ cards.append("""
302
+ <div class="empty-state">
303
+ All eligible profiles are already displayed.
304
+ </div>
305
+ """)
306
+
307
+ return "\n".join(cards), display_count, has_more
308
+
309
+
310
+ def search_hf_atlas(query, activity_filter, custom_days):
311
+ results_html, display_count, has_more = build_search_results(
312
+ query=query,
313
+ activity_filter=activity_filter,
314
+ custom_days=custom_days,
315
+ display_count=10,
316
+ )
317
+
318
+ more_button_update = gr.update(visible=has_more)
319
+
320
+ return results_html, display_count, more_button_update
321
+
322
+
323
+ def search_more_hf_atlas(query, activity_filter, custom_days, display_count):
324
+ if display_count is None:
325
+ display_count = 10
326
+
327
+ display_count = int(display_count)
328
+
329
+ if display_count <= 0:
330
+ display_count = 10
331
+
332
+ new_display_count = display_count + 10
333
+
334
+ results_html, final_display_count, has_more = build_search_results(
335
+ query=query,
336
+ activity_filter=activity_filter,
337
+ custom_days=custom_days,
338
+ display_count=new_display_count,
339
+ )
340
+
341
+ more_button_update = gr.update(visible=has_more)
342
+
343
+ return results_html, final_display_count, more_button_update
344
+
345
+
346
+ css = """
347
+ body {
348
+ background: radial-gradient(circle at top left, #172554 0%, #020617 35%, #020617 100%);
349
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
350
+ }
351
+
352
+ .gradio-container {
353
+ max-width: 980px !important;
354
+ }
355
+
356
+ #title {
357
+ font-size: 3.1em;
358
+ font-weight: 900;
359
+ text-align: center;
360
+ color: #e0f2fe;
361
+ text-shadow: 0 0 18px rgba(56, 189, 248, 0.45);
362
+ margin-bottom: 0;
363
+ }
364
+
365
+ #subtitle {
366
+ color: #bae6fd;
367
+ text-align: center;
368
+ margin-top: 0.6em;
369
+ margin-bottom: 2.2em;
370
+ font-size: 1.15em;
371
+ }
372
+
373
+ textarea {
374
+ background: rgba(15, 23, 42, 0.92) !important;
375
+ border: 1px solid rgba(125, 211, 252, 0.55) !important;
376
+ color: #e0f2fe !important;
377
+ border-radius: 18px !important;
378
+ }
379
+
380
+ input, select {
381
+ background: rgba(15, 23, 42, 0.90) !important;
382
+ border: 1px solid rgba(56, 189, 248, 0.35) !important;
383
+ color: #e0f2fe !important;
384
+ }
385
+
386
+ button {
387
+ background: linear-gradient(135deg, #38bdf8, #818cf8) !important;
388
+ border: none !important;
389
+ color: #020617 !important;
390
+ font-weight: 900 !important;
391
+ font-size: 1.08em !important;
392
+ border-radius: 18px !important;
393
+ box-shadow: 0 0 24px rgba(56, 189, 248, 0.35);
394
+ }
395
+
396
+ button:hover {
397
+ transform: scale(1.015);
398
+ box-shadow: 0 0 34px rgba(129, 140, 248, 0.55);
399
+ }
400
+
401
+ .result-card {
402
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.96), rgba(30, 41, 59, 0.88));
403
+ border: 1px solid rgba(125, 211, 252, 0.35);
404
+ border-radius: 24px;
405
+ padding: 22px;
406
+ margin: 18px 0;
407
+ box-shadow: 0 16px 44px rgba(0, 0, 0, 0.34);
408
+ }
409
+
410
+ .result-topline {
411
+ display: flex;
412
+ justify-content: space-between;
413
+ gap: 16px;
414
+ align-items: flex-start;
415
+ }
416
+
417
+ .rank {
418
+ color: #7dd3fc;
419
+ font-size: 0.92em;
420
+ font-weight: 800;
421
+ letter-spacing: 0.08em;
422
+ }
423
+
424
+ .username {
425
+ color: #e0f2fe !important;
426
+ font-size: 1.55em;
427
+ font-weight: 900;
428
+ text-decoration: none !important;
429
+ }
430
+
431
+ .username:hover {
432
+ color: #38bdf8 !important;
433
+ text-decoration: underline !important;
434
+ }
435
+
436
+ .fullname {
437
+ color: #cbd5e1;
438
+ margin-top: 4px;
439
+ font-size: 0.98em;
440
+ }
441
+
442
+ .score {
443
+ color: #020617;
444
+ background: linear-gradient(135deg, #67e8f9, #a5b4fc);
445
+ padding: 9px 13px;
446
+ border-radius: 999px;
447
+ font-weight: 900;
448
+ min-width: 90px;
449
+ text-align: center;
450
+ }
451
+
452
+ .details {
453
+ color: #cbd5e1;
454
+ background: rgba(2, 6, 23, 0.38);
455
+ border-left: 3px solid rgba(56, 189, 248, 0.65);
456
+ padding: 12px 14px;
457
+ margin-top: 16px;
458
+ border-radius: 14px;
459
+ }
460
+
461
+ .interests {
462
+ margin-top: 16px;
463
+ color: #e0f2fe;
464
+ line-height: 1.55;
465
+ }
466
+
467
+ .label {
468
+ color: #7dd3fc;
469
+ font-weight: 900;
470
+ margin-bottom: 6px;
471
+ text-transform: uppercase;
472
+ letter-spacing: 0.08em;
473
+ font-size: 0.78em;
474
+ }
475
+
476
+ .stats {
477
+ display: flex;
478
+ flex-wrap: wrap;
479
+ gap: 10px;
480
+ margin-top: 18px;
481
+ }
482
+
483
+ .stats span {
484
+ background: rgba(14, 165, 233, 0.10);
485
+ color: #bae6fd;
486
+ border: 1px solid rgba(125, 211, 252, 0.22);
487
+ border-radius: 999px;
488
+ padding: 7px 11px;
489
+ font-size: 0.92em;
490
+ }
491
+
492
+ .search-summary {
493
+ color: #bae6fd;
494
+ text-align: center;
495
+ background: rgba(15, 23, 42, 0.7);
496
+ border: 1px solid rgba(125, 211, 252, 0.25);
497
+ border-radius: 18px;
498
+ padding: 12px;
499
+ margin-bottom: 18px;
500
+ }
501
+
502
+ .empty-state {
503
+ text-align: center;
504
+ color: #bae6fd;
505
+ background: rgba(15, 23, 42, 0.75);
506
+ border: 1px solid rgba(125, 211, 252, 0.25);
507
+ border-radius: 18px;
508
+ padding: 24px;
509
+ margin-top: 16px;
510
+ }
511
+
512
+ .more-button-wrap {
513
+ margin-top: 10px;
514
+ margin-bottom: 30px;
515
+ }
516
+
517
+ .nebula {
518
+ position: fixed;
519
+ inset: 0;
520
+ pointer-events: none;
521
+ z-index: 0;
522
+ opacity: 0.40;
523
+ background:
524
+ radial-gradient(circle at 20% 20%, rgba(56,189,248,0.24), transparent 28%),
525
+ radial-gradient(circle at 80% 30%, rgba(129,140,248,0.22), transparent 30%),
526
+ radial-gradient(circle at 50% 80%, rgba(14,165,233,0.16), transparent 26%);
527
+ }
528
+ """
529
+
530
+
531
+ with gr.Blocks(css=css, theme=gr.themes.Base(), title="HF Atlas Explorer") as demo:
532
+ gr.HTML('<div class="nebula"></div>')
533
+ gr.HTML('<h1 id="title">🧭 HF Atlas Explorer</h1>')
534
+ gr.HTML(
535
+ '<p id="subtitle">Search Hugging Face profiles by AI/ML interests and filter by public activity.</p>'
536
+ )
537
+
538
+ query = gr.Textbox(
539
+ label="Search query",
540
+ placeholder="e.g. diffusion models, biomedical NLP, reinforcement learning, graph neural networks, robotics...",
541
+ lines=4,
542
+ )
543
+
544
+ with gr.Row():
545
+ activity_filter = gr.Dropdown(
546
+ choices=[
547
+ "No filter",
548
+ "Max age in days",
549
+ "Has known activity date",
550
+ "No known activity date",
551
+ ],
552
+ value="Max age in days",
553
+ label="Last public activity filter",
554
+ )
555
+
556
+ custom_days = gr.Number(
557
+ label="Max last_seen age in days, 0 = no limit",
558
+ value=365,
559
+ precision=0,
560
+ )
561
+
562
+ display_count_state = gr.State(value=0)
563
+
564
+ submit_btn = gr.Button("🔎 Search HF Atlas")
565
+
566
+ output = gr.HTML()
567
+
568
+ with gr.Row(elem_classes=["more-button-wrap"]):
569
+ more_btn = gr.Button("➕ Afficher plus", visible=False)
570
+
571
+ submit_btn.click(
572
+ fn=search_hf_atlas,
573
+ inputs=[query, activity_filter, custom_days],
574
+ outputs=[output, display_count_state, more_btn],
575
+ )
576
+
577
+ more_btn.click(
578
+ fn=search_more_hf_atlas,
579
+ inputs=[query, activity_filter, custom_days, display_count_state],
580
+ outputs=[output, display_count_state, more_btn],
581
+ )
582
+
583
+
584
+
585
+ demo.launch()