build-small-hackathon/hackathon-advisor
16
1from __future__ import annotations2 3from dataclasses import dataclass4from difflib import SequenceMatcher5import re6 7 8@dataclass(frozen=True)9class Correction:10 original: str11 canonical: str12 confidence: float13 14 def to_dict(self) -> dict:15 return {16 "original": self.original,17 "canonical": self.canonical,18 "confidence": round(self.confidence, 3),19 }20 21 22ALIASES: dict[str, tuple[str, ...]] = {23 "Nemotron": ("nemotron", "nemo tron", "neutron", "nemotran", "nemo-tron"),24 "MiniCPM5": ("minicpm5", "mini cpm5", "mini cpm", "open cpm", "opencpm5", "cpm five"),25 "EmbeddingGemma": ("embedding gemma", "embeddinggemma", "gemma embedding", "embedded gemma"),26 "ZeroGPU": ("zero gpu", "zerogpu", "zero-gpu", "zero g p u"),27 "Gradio Server": ("gradio server", "gradio.server", "server mode"),28 "Build Small Hackathon": ("build small", "build-small", "small hackathon"),29 "Off the Grid": ("off the grid", "off-grid", "offline badge"),30 "Well-Tuned": ("well tuned", "well-tuned", "fine tune", "finetune", "lora"),31 "Tiny Titan": ("tiny titan", "tiny tight end", "tiny-titan"),32 "Llama Champion": ("llama champion", "llama.cpp", "llama cpp", "llama badge"),33}34 35_TOKEN_RE = re.compile(r"[a-z0-9]+(?:[.-][a-z0-9]+)?", re.IGNORECASE)36 37 38def normalize_text(text: str) -> tuple[str, list[Correction]]:39 normalized = text40 corrections: list[Correction] = []41 spans = _candidate_spans(text)42 used: set[str] = set()43 44 for canonical, aliases in ALIASES.items():45 best: tuple[str, float] | None = None46 for alias in aliases:47 for span in spans:48 confidence = _similarity(alias, span)49 if confidence >= 0.88 and (best is None or confidence > best[1]):50 best = (span, confidence)51 if not best:52 continue53 54 original, confidence = best55 if original.lower() in used or original == canonical:56 continue57 used.add(original.lower())58 normalized = re.sub(re.escape(original), canonical, normalized, count=1, flags=re.IGNORECASE)59 corrections.append(Correction(original=original, canonical=canonical, confidence=confidence))60 61 return normalized, corrections62 63 64def _candidate_spans(text: str) -> list[str]:65 tokens = _TOKEN_RE.findall(text.lower())66 spans = set(tokens)67 for size in (2, 3):68 for index in range(max(0, len(tokens) - size + 1)):69 spans.add(" ".join(tokens[index : index + size]))70 return sorted(spans, key=len, reverse=True)71 72 73def _similarity(left: str, right: str) -> float:74 compact_left = re.sub(r"[^a-z0-9]", "", left.lower())75 compact_right = re.sub(r"[^a-z0-9]", "", right.lower())76 if not compact_left or not compact_right:77 return 0.078 if compact_left == compact_right:79 return 1.080 return SequenceMatcher(None, compact_left, compact_right).ratio()81 