CoolFace
Apppublic

build-small-hackathon/hackathon-advisor

sourceHugging Facemitupdated 3mo agoView on Hugging Face
16likes
dashboard.py536 linesDownload Raw Back to hackathon_advisor
1from __future__ import annotations2 3from collections import Counter, defaultdict4from collections.abc import Mapping, Sequence5import math6from typing import Any7 8from hackathon_advisor.data import (9    Project,10    ProjectIndex,11    normalize_project_tags,12    public_project_summary,13    public_project_title,14    tokenize,15)16from hackathon_advisor.quest_taxonomy import QUESTS, normalize_match, quest_profiles17from hackathon_advisor._text import utc_now18 19 20DASHBOARD_SCHEMA_VERSION = 121TSNE_RANDOM_STATE = 4222TSNE_MIN_PROJECTS = 323LINKS_PER_PROJECT = 224CLUSTER_LABEL_ALGORITHM = "distinctive-keywords-v1"25 26STOPWORDS = {27    "about",28    "agent",29    "app",30    "apps",31    "ai",32    "all",33    "an",34    "and",35    "are",36    "as",37    "at",38    "before",39    "assistant",40    "be",41    "been",42    "being",43    "build",44    "build-small",45    "build-small-hackathon",46    "built",47    "by",48    "demo",49    "face",50    "for",51    "from",52    "gradio",53    "hackathon",54    "hugging",55    "huggingface",56    "in",57    "is",58    "it",59    "its",60    "first",61    "local",62    "make",63    "makes",64    "made",65    "me",66    "model",67    "models",68    "my",69    "of",70    "on",71    "or",72    "our",73    "one",74    "project",75    "projects",76    "pro",77    "region",78    "run",79    "runs",80    "small",81    "space",82    "spaces",83    "submission",84    "the",85    "their",86    "them",87    "these",88    "they",89    "this",90    "those",91    "to",92    "tool",93    "tools",94    "try",95    "us",96    "use",97    "used",98    "uses",99    "using",100    "we",101    "with",102    "you",103    "your",104}105 106 107class DashboardError(ValueError):108    pass109 110 111def build_dashboard_payload(112    index: ProjectIndex,113    *,114    quest_matches: Mapping[str, Sequence[Mapping[str, Any]]] | None = None,115    quest_source: str = "",116    generated_at: str | None = None,117) -> dict[str, Any]:118    projects = list(index.projects)119    if len(projects) < TSNE_MIN_PROJECTS:120        raise DashboardError(f"dashboard atlas requires at least {TSNE_MIN_PROJECTS} projects")121 122    matrix = _embedding_matrix(index)123    coordinates = _tsne_coordinates(matrix)124    raw_cluster_labels = _cluster_labels(matrix)125    cluster_id_by_raw, clusters = _cluster_payloads(projects, coordinates, raw_cluster_labels)126    normalized_quest_matches = _normalize_quest_matches(projects, quest_matches)127    points = _point_payloads(projects, coordinates, raw_cluster_labels, cluster_id_by_raw, normalized_quest_matches)128    links = _nearest_links(projects, matrix)129    quest_report = _quest_report(points, normalized_quest_matches, quest_source)130    payload = {131        "schema_version": DASHBOARD_SCHEMA_VERSION,132        "generated_at": generated_at or utc_now(),133        "project_count": len(projects),134        "provenance": {135            "snapshot_generated_at": index.generated_at,136            "snapshot_source": index.source,137            "index_generated_at": index.index_generated_at,138            "index_algorithm": index.index_algorithm,139            "snapshot_digest": index.snapshot_digest,140            "embedding": index.embedding_metadata,141        },142        "layout": {143            "algorithm": "tsne",144            "metric": "cosine",145            "init": "pca",146            "random_state": TSNE_RANDOM_STATE,147            "perplexity": _tsne_perplexity(len(projects)),148        },149        "cluster_label_algorithm": CLUSTER_LABEL_ALGORITHM,150        "points": points,151        "links": links,152        "clusters": clusters,153        "quest_report": quest_report,154    }155    validate_dashboard_payload(payload)156    return payload157 158 159def validate_dashboard_payload(payload: Mapping[str, Any]) -> None:160    if payload.get("schema_version") != DASHBOARD_SCHEMA_VERSION:161        raise DashboardError("unsupported dashboard schema version")162    project_count = int(payload.get("project_count") or 0)163    if project_count < TSNE_MIN_PROJECTS:164        raise DashboardError("dashboard project count is too small")165    points = payload.get("points")166    if not isinstance(points, list) or len(points) != project_count:167        raise DashboardError("dashboard point count does not match project count")168    ids: set[str] = set()169    cluster_ids: set[str] = set()170    for point in points:171        if not isinstance(point, dict):172            raise DashboardError("dashboard points must be objects")173        project_id = str(point.get("id") or "")174        if not project_id or project_id in ids:175            raise DashboardError("dashboard points must have unique project ids")176        ids.add(project_id)177        x = float(point.get("x"))178        y = float(point.get("y"))179        if not 0.0 <= x <= 100.0 or not 0.0 <= y <= 100.0:180            raise DashboardError("dashboard point coordinates must be percentages")181        cluster_id = str(point.get("cluster_id") or "")182        if not cluster_id:183            raise DashboardError("dashboard point cluster id is missing")184        cluster_ids.add(cluster_id)185 186    clusters = payload.get("clusters")187    if not isinstance(clusters, list) or not clusters:188        raise DashboardError("dashboard clusters are missing")189    declared_cluster_ids = {str(cluster.get("id") or "") for cluster in clusters if isinstance(cluster, dict)}190    if cluster_ids - declared_cluster_ids:191        raise DashboardError("dashboard points reference missing clusters")192 193    links = payload.get("links")194    if not isinstance(links, list):195        raise DashboardError("dashboard links must be a list")196    for link in links:197        if str(link.get("source") or "") not in ids or str(link.get("target") or "") not in ids:198            raise DashboardError("dashboard link references an unknown project")199 200    quest_report = payload.get("quest_report")201    if not isinstance(quest_report, dict):202        raise DashboardError("dashboard quest report is missing")203    if quest_report.get("status") not in {"analyzed", "not_analyzed"}:204        raise DashboardError("dashboard quest report status is invalid")205 206 207def _embedding_matrix(index: ProjectIndex) -> Any:208    import numpy as np209 210    return np.asarray(index.project_vectors(), dtype=np.float32)211 212 213def _tsne_coordinates(matrix: Any) -> list[tuple[float, float]]:214    from sklearn.manifold import TSNE215 216    coords = TSNE(217        n_components=2,218        perplexity=_tsne_perplexity(int(matrix.shape[0])),219        init="pca",220        learning_rate="auto",221        max_iter=1000,222        metric="cosine",223        random_state=TSNE_RANDOM_STATE,224    ).fit_transform(matrix)225    return _scale_points(coords)226 227 228def _tsne_perplexity(count: int) -> int:229    return max(2, min(30, count // 4))230 231 232def _cluster_labels(matrix: Any) -> list[int]:233    from sklearn.cluster import KMeans234 235    count = int(matrix.shape[0])236    cluster_count = min(10, max(min(6, count), round(math.sqrt(count))))237    labels = KMeans(238        n_clusters=cluster_count,239        random_state=TSNE_RANDOM_STATE,240        n_init=20,241    ).fit_predict(matrix)242    return [int(label) for label in labels]243 244 245def _scale_points(points: Any, low: float = 3.0, high: float = 97.0) -> list[tuple[float, float]]:246    import numpy as np247 248    scaled = np.empty_like(points, dtype=np.float64)249    for axis in range(points.shape[1]):250        column = points[:, axis]251        minimum = float(column.min())252        maximum = float(column.max())253        span = maximum - minimum254        if span <= 1e-9:255            scaled[:, axis] = (low + high) / 2.0256        else:257            scaled[:, axis] = low + (column - minimum) / span * (high - low)258    return [(round(float(x), 4), round(float(y), 4)) for x, y in scaled]259 260 261def _cluster_payloads(262    projects: Sequence[Project],263    coordinates: Sequence[tuple[float, float]],264    raw_labels: Sequence[int],265) -> tuple[dict[int, str], list[dict[str, Any]]]:266    grouped: dict[int, list[int]] = defaultdict(list)267    for index, label in enumerate(raw_labels):268        grouped[int(label)].append(index)269 270    ordered_raw_labels = sorted(271        grouped,272        key=lambda label: (-len(grouped[label]), _cluster_center(coordinates, grouped[label])),273    )274    cluster_id_by_raw = {label: f"cluster-{position + 1}" for position, label in enumerate(ordered_raw_labels)}275    clusters: list[dict[str, Any]] = []276    corpus_document_frequency = _corpus_document_frequency(projects)277    for raw_label in ordered_raw_labels:278        indexes = grouped[raw_label]279        cluster_projects = [projects[index] for index in indexes]280        representatives = sorted(281            cluster_projects,282            key=lambda project: (project.likes, project.last_modified, project.title.lower()),283            reverse=True,284        )[:4]285        keywords = _cluster_keywords(286            cluster_projects,287            corpus_document_frequency=corpus_document_frequency,288            corpus_project_count=len(projects),289        )290        label = (291            " / ".join(word.title() for word in keywords[:2])292            if keywords293            else _representative_cluster_label(representatives)294        )295        clusters.append(296            {297                "id": cluster_id_by_raw[raw_label],298                "label": label,299                "keywords": keywords,300                "project_count": len(indexes),301                "center": {302                    "x": round(sum(coordinates[index][0] for index in indexes) / len(indexes), 4),303                    "y": round(sum(coordinates[index][1] for index in indexes) / len(indexes), 4),304                },305                "representative_projects": [project.to_public_dict() for project in representatives],306            }307        )308    return cluster_id_by_raw, clusters309 310 311def _cluster_center(coordinates: Sequence[tuple[float, float]], indexes: Sequence[int]) -> tuple[float, float]:312    return (313        sum(coordinates[index][0] for index in indexes) / len(indexes),314        sum(coordinates[index][1] for index in indexes) / len(indexes),315    )316 317 318def _corpus_document_frequency(projects: Sequence[Project]) -> Counter[str]:319    document_frequency: Counter[str] = Counter()320    for project in projects:321        document_frequency.update(set(_project_keyword_tokens(project)))322    return document_frequency323 324 325def _cluster_keywords(326    projects: Sequence[Project],327    *,328    corpus_document_frequency: Mapping[str, int],329    corpus_project_count: int,330) -> list[str]:331    counts: Counter[str] = Counter()332    document_frequency: Counter[str] = Counter()333    project_list = list(projects)334    for project in project_list:335        tokens = _project_keyword_tokens(project)336        counts.update(tokens)337        document_frequency.update(set(tokens))338 339    if not project_list:340        return []341 342    min_cluster_documents = 1 if len(project_list) <= 3 else 2343    scored: list[tuple[float, int, int, str]] = []344    for token, count in counts.items():345        cluster_documents = document_frequency[token]346        if cluster_documents < min_cluster_documents:347            continue348        corpus_documents = int(corpus_document_frequency.get(token) or 0)349        if corpus_documents <= 0:350            continue351        inverse_document_frequency = math.log((1 + corpus_project_count) / (1 + corpus_documents))352        if inverse_document_frequency <= 0.0:353            continue354        exclusivity = cluster_documents / corpus_documents355        coverage = cluster_documents / len(project_list)356        score = (357            (1.0 + math.log(count))358            * inverse_document_frequency359            * (0.35 + 0.65 * exclusivity)360            * (0.35 + 0.65 * coverage)361        )362        scored.append((score, cluster_documents, count, token))363 364    scored.sort(key=lambda item: (-item[0], -item[1], -item[2], item[3]))365    return [token for _score, _cluster_documents, _count, token in scored[:5]]366 367 368def _project_keyword_tokens(project: Project) -> list[str]:369    text = " ".join(370        [371            project.title,372            project.slug.replace("-", " ").replace("_", " "),373            project.summary,374            " ".join(normalize_project_tags(project.tags)),375            " ".join(project.models),376        ]377    )378    return [token for token in tokenize(text) if _is_cluster_keyword(token)]379 380 381def _is_cluster_keyword(token: str) -> bool:382    if token in STOPWORDS:383        return False384    if token.startswith("region"):385        return False386    if token.isdigit():387        return False388    return True389 390 391def _representative_cluster_label(projects: Sequence[Project]) -> str:392    labels: list[str] = []393    for project in projects:394        title = public_project_title(project.title)395        if title == "Untitled project":396            continue397        labels.append(title)398        if len(labels) == 2:399            break400    return " / ".join(labels) if labels else "Mixed projects"401 402 403def _normalize_quest_matches(404    projects: Sequence[Project],405    quest_matches: Mapping[str, Sequence[Mapping[str, Any]]] | None,406) -> dict[str, list[dict[str, Any]]]:407    project_ids = {project.id for project in projects}408    normalized = {project.id: [] for project in projects}409    if quest_matches is None:410        return normalized411    if set(quest_matches) != project_ids:412        missing = sorted(project_ids - set(quest_matches))413        extra = sorted(set(quest_matches) - project_ids)414        detail = []415        if missing:416            detail.append(f"missing {len(missing)} projects")417        if extra:418            detail.append(f"unknown {len(extra)} projects")419        raise DashboardError("quest analysis project coverage is invalid: " + ", ".join(detail))420    for project_id, matches in quest_matches.items():421        normalized[project_id] = [_normalize_quest_match(match) for match in matches]422    return normalized423 424 425def _normalize_quest_match(match: Mapping[str, Any]) -> dict[str, Any]:426    try:427        return normalize_match(match)428    except ValueError as error:429        raise DashboardError(f"invalid quest match: {error}") from error430 431 432def _point_payloads(433    projects: Sequence[Project],434    coordinates: Sequence[tuple[float, float]],435    raw_labels: Sequence[int],436    cluster_id_by_raw: Mapping[int, str],437    quest_matches: Mapping[str, Sequence[Mapping[str, Any]]],438) -> list[dict[str, Any]]:439    points: list[dict[str, Any]] = []440    for project, (x, y), raw_label in zip(projects, coordinates, raw_labels, strict=True):441        matches = list(quest_matches.get(project.id) or [])442        points.append(443            {444                "id": project.id,445                "title": public_project_title(project.title),446                "summary": public_project_summary(project.summary),447                "url": project.url,448                "host": project.host,449                "likes": project.likes,450                "sdk": project.sdk,451                "models": list(project.models),452                "tags": list(normalize_project_tags(project.tags)),453                "last_modified": project.last_modified,454                "x": x,455                "y": y,456                "cluster_id": cluster_id_by_raw[int(raw_label)],457                "quest_matches": matches,458                "quest_ids": [str(match["quest"]) for match in matches],459            }460        )461    return points462 463 464def _nearest_links(projects: Sequence[Project], matrix: Any) -> list[dict[str, Any]]:465    import numpy as np466 467    similarity = matrix @ matrix.T468    pairs: dict[tuple[int, int], float] = {}469    for index in range(len(projects)):470        order = np.argsort(similarity[index])[::-1]471        neighbors = [int(candidate) for candidate in order if int(candidate) != index][:LINKS_PER_PROJECT]472        for neighbor in neighbors:473            left, right = sorted((index, neighbor))474            pairs[(left, right)] = max(float(similarity[left, right]), pairs.get((left, right), -1.0))475    return [476        {477            "source": projects[left].id,478            "target": projects[right].id,479            "score": round(max(0.0, min(1.0, score)), 4),480        }481        for (left, right), score in sorted(pairs.items(), key=lambda item: (-item[1], item[0]))482    ]483 484 485def _quest_report(486    points: Sequence[Mapping[str, Any]],487    quest_matches: Mapping[str, Sequence[Mapping[str, Any]]],488    quest_source: str,489) -> dict[str, Any]:490    profiles = {profile["id"]: profile for profile in quest_profiles()}491    status = "analyzed" if quest_source else "not_analyzed"492    quests = []493    for quest in QUESTS:494        matched_points = [495            point496            for point in points497            if any(match["quest"] == quest for match in quest_matches.get(str(point["id"]), []))498        ]499        examples = sorted(500            matched_points,501            key=lambda point: (502                max(503                    (504                        float(match["confidence"])505                        for match in quest_matches.get(str(point["id"]), [])506                        if match["quest"] == quest507                    ),508                    default=0.0,509                ),510                int(point.get("likes") or 0),511            ),512            reverse=True,513        )[:4]514        profile = profiles.get(quest, {"label": quest, "description": ""})515        quests.append(516            {517                "id": quest,518                "label": profile["label"],519                "description": profile["description"],520                "project_count": len(matched_points),521                "examples": [522                    {523                        "id": point["id"],524                        "title": point["title"],525                        "url": point["url"],526                    }527                    for point in examples528                ],529            }530        )531    return {532        "status": status,533        "source": quest_source,534        "quests": quests,535    }536