huggimon / src /scoring.py
ImTamsi's picture
feat: initial HuggiMon Space MVP
f4e4f7c
Raw
History Blame Contribute Delete
6.82 kB
"""Compute HuggiMon card stats, type, rarity and moves from HF profile data."""
import math
from dataclasses import dataclass
from typing import List, Tuple
from src.hf_fetcher import HfProfileData
MAX_STAT = 100
TYPE_TAGS = {
"Code": ["code", "codegen", "programming", "codet5", "codebert", "starcoder"],
"Vision": ["vision", "image-classification", "object-detection", "diffusers", "image-to-text", "text-to-image"],
"Audio": ["audio", "speech", "automatic-speech-recognition", "text-to-speech", "voice-activity-detection"],
"NLP": ["nlp", "text-classification", "token-classification", "question-answering", "summarization", "translation"],
"Multimodal": ["multimodal", "image-to-text", "text-to-image", "visual-question-answering"],
"Agent": ["agent", "smolagents", "tool", "autonomous", "grpo"],
"Dataset": [],
}
@dataclass
class CardStats:
model: int
data: int
space: int
impact: int
community: int
docs: int
@property
def overall(self) -> int:
return int(
round(
(self.model + self.data + self.space + self.impact + self.community + self.docs) / 6
)
)
@dataclass
class CardData:
username: str
display_name: str
level: int
type: str
rarity: str
stats: CardStats
attacks: List[str]
passive: str
evolution: str
total_models: int
total_datasets: int
total_spaces: int
total_followers: int
total_downloads: int
total_likes: int
def _clamp(value: float) -> int:
return max(0, min(MAX_STAT, int(round(value))))
def _score_model(data: HfProfileData) -> int:
models = len(data.models)
likes = data.total_model_likes
downloads = data.total_model_downloads
score = models * 4 + math.log1p(likes) * 5 + math.log1p(downloads) * 1.5
return _clamp(score)
def _score_data(data: HfProfileData) -> int:
datasets = len(data.datasets)
likes = sum(d.likes for d in data.datasets)
downloads = sum(d.downloads for d in data.datasets)
score = datasets * 8 + math.log1p(likes) * 3 + math.log1p(downloads) * 1.2
return _clamp(score)
def _score_space(data: HfProfileData) -> int:
spaces = len(data.spaces)
likes = data.total_space_likes
score = spaces * 9 + math.log1p(likes) * 6
return _clamp(score)
def _score_impact(data: HfProfileData) -> int:
likes = data.total_likes
downloads = data.total_downloads
score = math.log1p(likes) * 8 + math.log1p(downloads) * 2.5
return _clamp(score)
def _score_community(data: HfProfileData) -> int:
followers = data.user.num_followers
discussions = data.user.num_discussions
score = followers * 0.8 + discussions * 2 + math.log1p(followers) * 5
return _clamp(score)
def _score_docs(data: HfProfileData) -> int:
all_repos = data.models + data.datasets + data.spaces
if not all_repos:
return 0
with_description = sum(1 for r in all_repos if r.description)
score = (with_description / len(all_repos)) * 80 + math.log1p(len(all_repos)) * 5
return _clamp(score)
def compute_stats(data: HfProfileData) -> CardStats:
return CardStats(
model=_score_model(data),
data=_score_data(data),
space=_score_space(data),
impact=_score_impact(data),
community=_score_community(data),
docs=_score_docs(data),
)
def _detect_type(data: HfProfileData, stats: CardStats) -> str:
tag_counts: dict[str, int] = {t: 0 for t in TYPE_TAGS}
for repo in data.models + data.datasets + data.spaces:
for tag in repo.tags:
lower = tag.lower()
for t, keywords in TYPE_TAGS.items():
if any(k in lower for k in keywords):
tag_counts[t] += 1
if len(data.datasets) > len(data.models) + len(data.spaces) and stats.data >= stats.model:
return "Dataset"
best = max(tag_counts, key=tag_counts.get)
if tag_counts[best] > 0:
return best
return "Code" if stats.model >= stats.data else "NLP"
def _rarity_from_overall(overall: int) -> str:
if overall >= 90:
return "Legendary"
if overall >= 75:
return "Epic"
if overall >= 55:
return "Rare"
return "Common"
def _level(overall: int) -> int:
return max(1, int(overall * 1.2))
def _moves(type_name: str, stats: CardStats) -> Tuple[List[str], str, str]:
attacks = []
if stats.model >= 70:
attacks.append("Model Overload")
elif stats.model >= 40:
attacks.append("Fine-tune Blast")
else:
attacks.append("Tiny Tune")
if stats.data >= 70:
attacks.append("Dataset Tsunami")
elif stats.data >= 40:
attacks.append("Dataset Drop")
else:
attacks.append("Data Sample")
if stats.space >= 70:
attacks.append("Space Storm")
elif stats.space >= 40:
attacks.append("Space Builder")
else:
attacks.append("Space Launch")
if stats.impact >= 80:
attacks.append("Viral Release")
# Trim to 2 best attacks by stat priority
priority = sorted(
[("Model Overload", stats.model), ("Dataset Tsunami", stats.data), ("Space Storm", stats.space)],
key=lambda x: x[1],
reverse=True,
)[:2]
attacks = [a for a, _ in priority]
passive_map = {
"Code": "Open Source Aura",
"Vision": "Pixel Precision",
"Audio": "Wave Resonance",
"NLP": "Token Mastery",
"Multimodal": "Fusion Core",
"Agent": "Toolformer Soul",
"Dataset": "Data Curator",
}
passive = passive_map.get(type_name, "Hub Spirit")
if stats.overall >= 90:
evolution = "Contributor → Builder → Hub Legend"
elif stats.overall >= 75:
evolution = "Contributor → Builder → Architect"
elif stats.overall >= 55:
evolution = "Contributor → Builder"
else:
evolution = "Contributor"
return attacks, passive, evolution
def build_card(data: HfProfileData) -> CardData:
stats = compute_stats(data)
overall = stats.overall
type_name = _detect_type(data, stats)
rarity = _rarity_from_overall(overall)
level = _level(overall)
attacks, passive, evolution = _moves(type_name, stats)
return CardData(
username=data.user.username,
display_name=data.user.display_name or data.user.username,
level=level,
type=type_name,
rarity=rarity,
stats=stats,
attacks=attacks,
passive=passive,
evolution=evolution,
total_models=len(data.models),
total_datasets=len(data.datasets),
total_spaces=len(data.spaces),
total_followers=data.user.num_followers,
total_downloads=data.total_downloads,
total_likes=data.total_likes,
)