Skip to content

Commit 2a4b7cf

Browse files
author
Hermes
committed
Merge branch 'feature/v0.4.0-profile' into main
v0.4.0 profile improvements: - 19+ title variety (Discord Whisperer no longer dominant) - top-2 shills + top-2 haters per user (multiple brand pills) - posting style as multiple pills (one per trait, flex-wrap) - brand lover/hater as AWARD (Mimo hater, MiniMax lover format) - awards badge legend now includes all new awards - Mod + Developer + Contributor pills (hardcoded user lists) - removed Vibe section (was always 'mood unavailable')
2 parents cbeabb1 + 696e42e commit 2a4b7cf

2 files changed

Lines changed: 220 additions & 75 deletions

File tree

build_index.py

Lines changed: 199 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -160,20 +160,69 @@ def _run(args):
160160
HATER_MIN_NEG_RATIO = 5.0 # Min negative:positive ratio to qualify as hater (was 1:3)
161161
USE_WORD_BOUNDARY = True # Use word-boundary regex in brand keyword matching
162162

163-
# ─── v0.3.0: Title assignment rules (ISSUE #2) ─────────────────────
164-
# Priority order (first match wins). Each entry is (label, emoji, condition_fn_string).
165-
# Conditions are checked in order top-to-bottom.
163+
# ─── v0.4.0: Title assignment rules (ISSUE #1) ─────────────────────
164+
def _peak_hour_bucket(s):
165+
"""Return time-of-day bucket name or None."""
166+
hour_dist = s.get("hour_dist", {}) or {}
167+
if not hour_dist:
168+
return None
169+
peak = max(hour_dist, key=hour_dist.get)
170+
if peak < 6:
171+
return "early_bird"
172+
if peak < 12:
173+
return "morning"
174+
if peak < 14:
175+
return "lunch"
176+
if peak < 18:
177+
return "afternoon"
178+
return "night_owl"
179+
166180
TITLE_RULES = [
167-
# (priority_order, "title", "emoji", condition_description)
168-
# Each checked in assign_title() in priority order
181+
# (priority, name, emoji, check_fn(stats_dict, rank_idx))
182+
# Lower number = more specific, checked first
183+
(1, "Foundation", "\U0001F451",
184+
lambda s, r: r is not None and r <= TITLE_FOUNDATION_RANK),
185+
(2, "OG", "\U0001F3DB\uFE0F",
186+
lambda s, r: ((s.get("first_seen") or "9999")[:10] < TITLE_OG_FIRST_SEEN
187+
and (s.get("total_chunks") or 0) >= TITLE_OG_MIN_CHUNKS)),
188+
(3, "Veteran", "\U0001F396\uFE0F",
189+
lambda s, r: ((s.get("first_seen") or "9999")[:10] < TITLE_VETERAN_FIRST_SEEN
190+
and (s.get("max_streak") or 0) >= TITLE_VETERAN_MIN_STREAK)),
191+
(4, "Pioneer", "\U0001F48E",
192+
lambda s, r: ((s.get("first_seen") or "9999")[:10] < TITLE_PIONEER_FIRST_SEEN)),
193+
(5, "Globetrotter", "\U0001F30D",
194+
lambda s, r: (len(s.get("channels", set()) or set()) >= TITLE_GLOBETROTTER_MIN_CHANNELS
195+
and (s.get("total_chunks") or 0) >= TITLE_GLOBETROTTER_MIN_CHUNKS)),
196+
(6, "Trailblazer", "\U0001F680",
197+
lambda s, r: (s.get("total_chunks") or 0) >= TITLE_TRAILBLAZER_MIN_CHUNKS),
198+
(7, "Discord Whisperer", "\U0001F5E3\uFE0F",
199+
lambda s, r: ((s.get("msg_per_chunk") or 0) > TITLE_WHISPERER_MIN_MSG_RATIO
200+
and (s.get("total_chunks") or 0) >= TITLE_WHISPERER_MIN_CHUNKS)),
201+
(8, "Polymath", "\U0001F52C",
202+
lambda s, r: len(s.get("channels", set()) or set()) >= TITLE_POLYMATH_MIN_CHANNELS),
203+
(9, "Sage", "\U0001F9E0",
204+
lambda s, r: ((s.get("msg_per_chunk") or 0) > TITLE_SAGE_MIN_MSG_PER_CHUNK
205+
and (s.get("total_chunks") or 0) >= TITLE_SAGE_MIN_CHUNKS)),
206+
(10, "Night Owl", "\U0001F989",
207+
lambda s, r: _peak_hour_bucket(s) == "night_owl"),
208+
(11, "Early Bird", "\U0001F305",
209+
lambda s, r: _peak_hour_bucket(s) == "early_bird"),
210+
(12, "Morning Poster", "\u2615",
211+
lambda s, r: _peak_hour_bucket(s) == "morning"),
212+
(13, "Lunch Break", "\U0001F96A",
213+
lambda s, r: _peak_hour_bucket(s) == "lunch"),
214+
(14, "Afternoon Poster", "\U0001F31E",
215+
lambda s, r: _peak_hour_bucket(s) == "afternoon"),
216+
(15, "Lurker", "\U0001F575\uFE0F",
217+
lambda s, r: ((s.get("total_chunks") or 0) >= TITLE_LURKER_MIN_CHUNKS
218+
and (s.get("msg_per_chunk") or 0) < TITLE_LURKER_MAX_MSG_RATIO)),
219+
(16, "Linker", "\U0001F517",
220+
lambda s, r: (s.get("link_rate") or 0) > TITLE_LINKER_MIN_RATE),
221+
(17, "Code Wizard", "\U0001F9D9",
222+
lambda s, r: (s.get("code_rate") or 0) > TITLE_CODE_WIZARD_MIN_RATE),
223+
(18, "Builder", "\U0001F3D7\uFE0F",
224+
lambda s, r: (s.get("total_chunks") or 0) >= TITLE_BUILDER_MIN_CHUNKS),
169225
]
170-
# Title thresholds: checked by assign_title(). Rarest/most prestigious first.
171-
# Top 5 by rank -> "Pioneer" (uses rank_idx, handled during leaderboard building)
172-
# msg_per_chunk > 15 -> "Sage"
173-
# streaks >= 30 -> "Streak Master"
174-
# 100+ chunks -> "Trailblazer"
175-
# 4+ channels active -> "Polymath"
176-
# 50+ chunks -> "Builder"
177226

178227
# ─── v0.3.0: GPU tier emojis (ISSUE #8, #17) ───────────────────────
179228
GPU_TIER_EMOJI = {
@@ -187,6 +236,39 @@ def _run(args):
187236
"integrated": "\U0001F4BB",
188237
}
189238

239+
# ─── v0.4.0: Title rule thresholds (ISSUE #1) ────────────
240+
TITLE_FOUNDATION_RANK = 5
241+
TITLE_OG_FIRST_SEEN = "2025-01-01"
242+
TITLE_OG_MIN_CHUNKS = 50
243+
TITLE_PIONEER_FIRST_SEEN = "2025-01-01"
244+
TITLE_VETERAN_FIRST_SEEN = "2025-06-01"
245+
TITLE_VETERAN_MIN_STREAK = 30
246+
TITLE_TRAILBLAZER_MIN_CHUNKS = 100
247+
TITLE_BUILDER_MIN_CHUNKS = 50
248+
TITLE_POLYMATH_MIN_CHANNELS = 4
249+
TITLE_SAGE_MIN_MSG_PER_CHUNK = 15
250+
TITLE_SAGE_MIN_CHUNKS = 10
251+
TITLE_WHISPERER_MIN_MSG_RATIO = 20
252+
TITLE_WHISPERER_MIN_CHUNKS = 20
253+
TITLE_LURKER_MIN_CHUNKS = 50
254+
TITLE_LURKER_MAX_MSG_RATIO = 3
255+
TITLE_LINKER_MIN_RATE = 0.15
256+
TITLE_CODE_WIZARD_MIN_RATE = 0.3
257+
TITLE_EMOJI_MASTER_MIN_RATE = 0.4
258+
TITLE_QUESTIONER_MIN_RATE = 0.4
259+
TITLE_GLOBETROTTER_MIN_CHANNELS = 4
260+
TITLE_GLOBETROTTER_MIN_CHUNKS = 50
261+
262+
# ─── v0.4.0: Brand lover/hater thresholds (ISSUE #13) ──────
263+
BRAND_LOVER_AWARD_MIN_TOTAL = 10
264+
BRAND_LOVER_AWARD_MIN_POS_RATIO = 3.0
265+
BRAND_HATER_AWARD_MIN_TOTAL = 10
266+
BRAND_HATER_AWARD_MIN_NEG_RATIO = 3.0
267+
268+
# ─── v0.4.0: Known community roles (ISSUE #15) ─────────────
269+
MOD_USERS = {"teknium"}
270+
DEVELOPER_USERS = {"4rgo", "teknium"}
271+
190272
# ─── T11: GPU power ranking list ────────────────────────────────────
191273
GPU_LIST = [
192274
( 1, "GB200 Grace+Blackwell", "384GB (2x B200)", "frontier", "#facc15"),
@@ -517,12 +599,14 @@ def assign_medal(rank, name):
517599
return ["ribbon", "Ribbon"]
518600

519601

520-
def assign_awards(user_stats):
602+
def assign_awards(user_stats, author_name=None):
521603
"""Stable contribution awards from lifetime stats. Only go up.
522604
523605
ISSUE #4: 8 new awards added (Linker, Convo-starter, Emoji Master,
524606
Questioner, Helper, Mentor, OG, Resurrected). Existing awards have
525607
priority (checked first). Cap at 2 awards per user.
608+
ISSUE #13: Brand lover/hater awards added.
609+
ISSUE #15: Mod/Developer/Contributor role pills added (separate field).
526610
"""
527611
awards = []
528612
channels = len(user_stats.get("channels", set()) or set())
@@ -587,33 +671,31 @@ def assign_awards(user_stats):
587671
if max_gap_days > 120:
588672
awards.append(["resurrected", "Resurrected"])
589673

674+
# ISSUE #13: Brand lover/hater awards (checked after standard awards)
675+
if len(awards) < 2:
676+
_shills = user_stats.get("shill_brands", []) or []
677+
for _s in _shills:
678+
_bm = BRAND_META.get(_s["brand"])
679+
if _bm and (_s.get("count") or 0) >= BRAND_LOVER_AWARD_MIN_TOTAL:
680+
awards.append(["lover", f"{_bm[1]} lover"])
681+
break
682+
683+
if len(awards) < 2:
684+
_haters = user_stats.get("hater_brands", []) or []
685+
for _h in _haters:
686+
_bm = BRAND_META.get(_h["brand"])
687+
if _bm and (_h.get("count") or 0) >= BRAND_HATER_AWARD_MIN_TOTAL:
688+
awards.append(["hater", f"{_bm[1]} hater"])
689+
break
690+
590691
return awards[:2]
591692

592693

593694
def assign_title(s, rank_idx=None):
594-
"""Return a single per-user title string based on strongest contribution signal.
595-
596-
ISSUE #2: Priority-ordered checks. First match wins.
597-
Titles: Pioneer, Sage, Streak Master, Trailblazer, Polymath, Builder.
598-
"""
599-
chunks = s.get("total_chunks", 0) or 0
600-
msgs = s.get("total_messages", 0) or 0
601-
channels_n = len(s.get("channels", set()) or set())
602-
msg_per_chunk = s.get("msg_per_chunk", 0) or 0
603-
max_streak = s.get("max_streak", 0) or 0
604-
605-
if rank_idx is not None and rank_idx <= 5:
606-
return "\U0001F48E Pioneer"
607-
if msg_per_chunk > 15 and chunks > 10:
608-
return "\U0001F9E0 Sage"
609-
if max_streak >= 30:
610-
return "\U0001F525 Streak Master"
611-
if chunks >= 100:
612-
return "\U0001F680 Trailblazer"
613-
if channels_n >= 4:
614-
return "\U0001F9EC Polymath"
615-
if chunks >= 50:
616-
return "\U0001F6E0\uFE0F Builder"
695+
"""Evaluate all TITLE_RULES and return the best match (highest priority)."""
696+
for _priority, _name, _emoji, _check_fn in TITLE_RULES:
697+
if _check_fn(s, rank_idx):
698+
return f"{_emoji} {_name}"
617699
return None
618700

619701

@@ -706,6 +788,74 @@ def compute_style_heuristic(s):
706788
return None
707789

708790

791+
def compute_style_pills(s):
792+
"""Return a list of individual style pill strings (ISSUE #10).
793+
794+
Each pill is one trait: channel activity, time of day, message length,
795+
linker, code wizard, emoji master, questioner.
796+
"""
797+
if not s.get("total_chunks"):
798+
return []
799+
pills = []
800+
chunks = s.get("total_chunks", 0)
801+
msgs = s.get("total_messages", 0)
802+
chans = sorted(s.get("channels", set()) or set())
803+
chan_short = {"hermes-agent": "hermes", "community-projects-showcase": "projects",
804+
"plugins-skills-and-skins": "plugins", "developers": "devs"}
805+
chans_disp = [chan_short.get(c, c) for c in chans]
806+
807+
# Channel activity
808+
if chunks <= 3:
809+
if chans_disp:
810+
chan_str = f"in #{chans_disp[0]}" if len(chans_disp) == 1 else f"in #{' + #'.join(chans_disp)}"
811+
pills.append(f"Brief contributor {chan_str}")
812+
return pills
813+
if len(chans_disp) == 1:
814+
pills.append(f"active in #{chans_disp[0]}")
815+
elif len(chans_disp) <= 3:
816+
pills.append(f"active in #{' + #'.join(chans_disp)}")
817+
else:
818+
pills.append(f"polymath across all {len(chans_disp)} channels")
819+
820+
# Time of day
821+
hour_dist = s.get("hour_dist", {}) or {}
822+
if hour_dist:
823+
peak_hour = max(hour_dist, key=hour_dist.get)
824+
if peak_hour < 6:
825+
pills.append("night owl (UTC)")
826+
elif peak_hour < 12:
827+
pills.append("morning poster (UTC)")
828+
elif peak_hour < 18:
829+
pills.append("afternoon poster (UTC)")
830+
else:
831+
pills.append("evening poster (UTC)")
832+
833+
# Message length
834+
avg_msg_len = round(s.get("total_msg_len", 0) / max(msgs, 1))
835+
if avg_msg_len > 350:
836+
pills.append(f"long-form (avg {avg_msg_len} chars)")
837+
elif avg_msg_len < 100:
838+
pills.append(f"concise (avg {avg_msg_len} chars)")
839+
else:
840+
pills.append(f"mid-length (avg {avg_msg_len} chars)")
841+
842+
# Style traits
843+
link_rate = s.get("link_rate", 0) or 0
844+
if link_rate > 0.15:
845+
pills.append("linker")
846+
code_rate = s.get("code_rate", 0) or 0
847+
if code_rate > 0.3:
848+
pills.append("code wizard")
849+
emoji_rate = s.get("emoji_rate", 0) or 0
850+
if emoji_rate > 0.4:
851+
pills.append("emoji master")
852+
q_rate = s.get("question_rate", 0) or 0
853+
if q_rate > 0.4:
854+
pills.append("questioner")
855+
856+
return pills
857+
858+
709859
# ─── T10: Brand detection ─────────────────────────────────────────
710860
def _match_brand_kw(content, kw):
711861
"""Match a keyword against content — supports \\b regex patterns."""
@@ -753,17 +903,17 @@ def assign_shill_hater(brand_sentiment):
753903
continue
754904
# All positive (no negative mentions ever)
755905
if pos > 0 and neg == 0 and pos >= BRAND_MENTION_THRESHOLD:
756-
shill.append({"brand": brand, "count": pos, "ratio": 9999.99})
906+
shill.append({"brand": brand, "count": pos, "ratio": 9999.99, "total": total})
757907
# All negative (no positive mentions ever)
758908
elif neg > 0 and pos == 0 and neg >= BRAND_MENTION_THRESHOLD:
759-
hater.append({"brand": brand, "count": neg, "ratio": 0.0})
909+
hater.append({"brand": brand, "count": neg, "ratio": 0.0, "total": total})
760910
# Mixed: check ratios
761911
elif pos + neg >= BRAND_MENTION_THRESHOLD:
762912
ratio = pos / max(neg, 1)
763913
if ratio >= SHILL_MIN_POS_RATIO:
764-
shill.append({"brand": brand, "count": pos, "ratio": round(ratio, 2)})
914+
shill.append({"brand": brand, "count": pos, "ratio": round(ratio, 2), "total": total})
765915
elif ratio <= (1.0 / HATER_MIN_NEG_RATIO):
766-
hater.append({"brand": brand, "count": neg, "ratio": round(ratio, 2)})
916+
hater.append({"brand": brand, "count": neg, "ratio": round(ratio, 2), "total": total})
767917
return shill, hater
768918

769919

@@ -1191,8 +1341,11 @@ def main():
11911341
for rank_idx, (author, s) in enumerate(_ranked, start=1):
11921342
tc = s["total_chunks"]
11931343
medal = assign_medal(rank_idx, author)
1194-
awards = assign_awards(s)
1344+
awards = assign_awards(s, author)
11951345
title = assign_title(s, rank_idx)
1346+
# Cap shill/hater at top-2 by total mention count (ISSUE #9)
1347+
_shills = sorted(s.get("shill_brands", []) or [], key=lambda x: x.get("total", 0), reverse=True)[:2]
1348+
_haters = sorted(s.get("hater_brands", []) or [], key=lambda x: x.get("total", 0), reverse=True)[:2]
11961349
leaderboard.append({
11971350
"name": author, "total_chunks": tc,
11981351
"total_messages": s["total_messages"],
@@ -1206,10 +1359,13 @@ def main():
12061359
"awards": awards,
12071360
"title": title,
12081361
"style_heuristic": compute_style_heuristic(s),
1362+
"style_pills": compute_style_pills(s),
12091363
"style_summary": STYLE_CACHE.get(author, {}).get("summary"),
1210-
"shill": s.get("shill_brands", []),
1211-
"hater": s.get("hater_brands", []),
1364+
"shill": _shills,
1365+
"hater": _haters,
12121366
"developed": s.get("developed_repos", []),
1367+
"mod": author in MOD_USERS,
1368+
"developer": author in DEVELOPER_USERS,
12131369
})
12141370

12151371
# Per-user mood context (T14a) — last 20 chunks sorted by recency

0 commit comments

Comments
 (0)