feat: enhance discovery + cache + comparison + add visuals + more
#2
by Pedroamaral - opened
- .dockerignore +7 -0
- Dockerfile +11 -4
- app.py +805 -51
- data/.gitkeep +0 -0
- data/popularity.json +1 -0
- docker-compose.yml +9 -0
- src/popularity_store.py +129 -0
- src/streamlit_app.py +805 -51
.dockerignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
__pycache__
|
| 3 |
+
*.pyc
|
| 4 |
+
.venv
|
| 5 |
+
venv/
|
| 6 |
+
.env
|
| 7 |
+
*.log
|
Dockerfile
CHANGED
|
@@ -1,20 +1,27 @@
|
|
| 1 |
FROM python:3.13.5-slim
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
RUN apt-get update && apt-get install -y \
|
| 6 |
build-essential \
|
| 7 |
curl \
|
| 8 |
git \
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
| 11 |
COPY requirements.txt ./
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
|
| 16 |
EXPOSE 8501
|
| 17 |
|
| 18 |
-
HEALTHCHECK
|
|
|
|
| 19 |
|
| 20 |
ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
|
|
|
| 1 |
FROM python:3.13.5-slim
|
| 2 |
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 4 |
+
PYTHONUNBUFFERED=1 \
|
| 5 |
+
PIP_NO_CACHE_DIR=1
|
| 6 |
+
|
| 7 |
WORKDIR /app
|
| 8 |
|
| 9 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
build-essential \
|
| 11 |
curl \
|
| 12 |
git \
|
| 13 |
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
|
| 15 |
COPY requirements.txt ./
|
| 16 |
+
RUN pip3 install --upgrade pip && pip3 install -r requirements.txt
|
| 17 |
|
| 18 |
+
COPY src/ ./src/
|
| 19 |
+
COPY app.py ./app.py
|
| 20 |
+
COPY data/ ./data/
|
| 21 |
|
| 22 |
EXPOSE 8501
|
| 23 |
|
| 24 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
|
| 25 |
+
CMD curl --fail http://localhost:8501/_stcore/health
|
| 26 |
|
| 27 |
ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
app.py
CHANGED
|
@@ -2,9 +2,12 @@ import streamlit as st
|
|
| 2 |
import pandas as pd
|
| 3 |
from huggingface_hub import HfApi
|
| 4 |
import requests
|
|
|
|
| 5 |
import re
|
| 6 |
import concurrent.futures
|
| 7 |
import logging
|
|
|
|
|
|
|
| 8 |
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
|
| 9 |
|
| 10 |
logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
@@ -20,13 +23,73 @@ st.markdown(
|
|
| 20 |
"allowing you to sort and compare them easily. Click on any column header to sort!"
|
| 21 |
)
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
@retry(
|
| 24 |
retry=retry_if_exception_type((requests.exceptions.ConnectionError, requests.exceptions.Timeout, requests.exceptions.HTTPError)),
|
| 25 |
stop=stop_after_attempt(3),
|
| 26 |
wait=wait_exponential(multiplier=1, min=2, max=10)
|
| 27 |
)
|
| 28 |
def fetch_url(url):
|
| 29 |
-
response =
|
| 30 |
# Raise HTTPError for rate limits (429) or server errors (5xx) to trigger a retry
|
| 31 |
if response.status_code in (429, 500, 502, 503, 504):
|
| 32 |
response.raise_for_status()
|
|
@@ -40,14 +103,14 @@ def fetch_model_info(model):
|
|
| 40 |
refusals_str = None
|
| 41 |
refusal_rate = None
|
| 42 |
initial_refusals_str = None
|
| 43 |
-
|
| 44 |
# Download the README.md via raw URL for high-speed fetching
|
| 45 |
url = f"https://huggingface.co/{model_id}/raw/main/README.md"
|
| 46 |
try:
|
| 47 |
response = fetch_url(url)
|
| 48 |
if response.status_code == 200:
|
| 49 |
readme_text = response.text
|
| 50 |
-
|
| 51 |
# Extract KL divergence using Regex
|
| 52 |
# Matches formats like: "KL divergence | 0.0033"
|
| 53 |
kl_match = re.search(r"(?i)KL\s*divergence[^a-zA-Z\d\n]*?([\d\.]+)", readme_text)
|
|
@@ -56,7 +119,7 @@ def fetch_model_info(model):
|
|
| 56 |
kl_div = float(kl_match.group(1))
|
| 57 |
except ValueError as e:
|
| 58 |
logging.warning(f"Could not parse KL divergence for {model_id}: {e}")
|
| 59 |
-
|
| 60 |
# Extract Refusals using Regex
|
| 61 |
# Matches formats like: "Refusals | 15/100" or "15 / 100"
|
| 62 |
for line in readme_text.split('\n'):
|
|
@@ -65,19 +128,19 @@ def fetch_model_info(model):
|
|
| 65 |
fractions = re.findall(r"(\d+)\s*/\s*(\d+)", line)
|
| 66 |
if not fractions:
|
| 67 |
continue
|
| 68 |
-
|
| 69 |
if 'initial' in line_lower and initial_refusals_str is None:
|
| 70 |
initial_refusals_str = f"{fractions[0][0]}/{fractions[0][1]}"
|
| 71 |
elif 'initial' not in line_lower and refusals_str is None:
|
| 72 |
refusals_str = f"{fractions[0][0]}/{fractions[0][1]}"
|
| 73 |
try:
|
| 74 |
-
refusal_rate = (int(fractions[0][0]) / int(fractions[0][1])) * 100
|
| 75 |
except (ValueError, ZeroDivisionError) as e:
|
| 76 |
logging.warning(f"Could not parse refusals for {model_id}: {e}")
|
| 77 |
continue
|
| 78 |
if len(fractions) >= 2 and initial_refusals_str is None:
|
| 79 |
initial_refusals_str = f"{fractions[1][0]}/{fractions[1][1]}"
|
| 80 |
-
|
| 81 |
except requests.exceptions.RequestException as e:
|
| 82 |
logging.warning(f"Error fetching README for {model_id}: {e}")
|
| 83 |
except Exception as e:
|
|
@@ -98,7 +161,7 @@ def fetch_model_info(model):
|
|
| 98 |
"URL": f"https://huggingface.co/{model_id}"
|
| 99 |
}
|
| 100 |
|
| 101 |
-
# Check for reproducibility record first and then fallback to parsing data from readme.md
|
| 102 |
def get_model_data(model):
|
| 103 |
model_id = model.id
|
| 104 |
url = f"https://huggingface.co/{model_id}/raw/main/reproduce/reproduce.json"
|
|
@@ -134,58 +197,749 @@ def get_model_data(model):
|
|
| 134 |
logging.error(f"Unexpected error processing JSON for {model_id}: {e}")
|
| 135 |
return fetch_model_info(model)
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
api = HfApi()
|
| 141 |
-
|
| 142 |
# Query all models using the Hugging Face Hub `filter` parameter
|
| 143 |
models = list(api.list_models(filter="heretic"))
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
data = []
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
| 154 |
for res in results:
|
| 155 |
if res is not None:
|
| 156 |
data.append(res)
|
| 157 |
-
|
| 158 |
return data
|
| 159 |
|
| 160 |
-
#
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
else:
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import pandas as pd
|
| 3 |
from huggingface_hub import HfApi
|
| 4 |
import requests
|
| 5 |
+
from requests.adapters import HTTPAdapter
|
| 6 |
import re
|
| 7 |
import concurrent.futures
|
| 8 |
import logging
|
| 9 |
+
from datetime import datetime, timezone
|
| 10 |
+
from types import SimpleNamespace
|
| 11 |
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
|
| 12 |
|
| 13 |
logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
|
|
| 23 |
"allowing you to sort and compare them easily. Click on any column header to sort!"
|
| 24 |
)
|
| 25 |
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
# Filter config (single source of truth)
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
FILTER_DEFAULTS = {
|
| 30 |
+
"query": "",
|
| 31 |
+
"date_range": "All time",
|
| 32 |
+
"unknowns": "Include unknowns",
|
| 33 |
+
"kl_range": (0.0, 0.5),
|
| 34 |
+
"refusal_range": (0.0, 100.0),
|
| 35 |
+
"min_likes": 0,
|
| 36 |
+
"min_downloads": 0,
|
| 37 |
+
"show_quants": False,
|
| 38 |
+
"sort_by": "KL Divergence (low β high)",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
DATE_OPTIONS = {
|
| 42 |
+
"All time": None,
|
| 43 |
+
"Last week": 7,
|
| 44 |
+
"Last month": 30,
|
| 45 |
+
"Last 6 months": 182,
|
| 46 |
+
"Last year": 365,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
UNKNOWN_OPTIONS = (
|
| 50 |
+
"Include unknowns",
|
| 51 |
+
"Hide unknowns",
|
| 52 |
+
"Only unknowns",
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
SORT_OPTIONS = {
|
| 56 |
+
"KL Divergence (low β high)": ("KL Divergence", True),
|
| 57 |
+
"KL Divergence (high β low)": ("KL Divergence", False),
|
| 58 |
+
"Refusal Rate (%) (low β high)": ("Refusal Rate (%)", True),
|
| 59 |
+
"Refusal Rate (%) (high β low)": ("Refusal Rate (%)", False),
|
| 60 |
+
"Likes (high β low)": ("Likes", False),
|
| 61 |
+
"Downloads (high β low)": ("Downloads", False),
|
| 62 |
+
"Model (A β Z)": ("Model", True),
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
QUANT_PATTERNS = re.compile(r'(?i)(gguf|mlx|awq|nvfp4|gptq|exl[23]|-quant|int8|int4|oq[1-8]|mxfp[48])')
|
| 66 |
+
QUANT_RE = r"(?i)(gguf|mlx|awq|nvfp4|gptq|exl[23]|-quant|int8|int4|oq[1-8]|mxfp[48])"
|
| 67 |
+
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
+
# Network: reused Session + retry
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
_SESSION = None
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _get_session():
|
| 75 |
+
global _SESSION
|
| 76 |
+
if _SESSION is None:
|
| 77 |
+
s = requests.Session()
|
| 78 |
+
adapter = HTTPAdapter(pool_connections=20, pool_maxsize=20, max_retries=0)
|
| 79 |
+
s.mount("https://", adapter)
|
| 80 |
+
s.mount("http://", adapter)
|
| 81 |
+
s.headers.update({"User-Agent": "Heretic-Models-Explorer/1.0"})
|
| 82 |
+
_SESSION = s
|
| 83 |
+
return _SESSION
|
| 84 |
+
|
| 85 |
+
|
| 86 |
@retry(
|
| 87 |
retry=retry_if_exception_type((requests.exceptions.ConnectionError, requests.exceptions.Timeout, requests.exceptions.HTTPError)),
|
| 88 |
stop=stop_after_attempt(3),
|
| 89 |
wait=wait_exponential(multiplier=1, min=2, max=10)
|
| 90 |
)
|
| 91 |
def fetch_url(url):
|
| 92 |
+
response = _get_session().get(url, timeout=(3.05, 5))
|
| 93 |
# Raise HTTPError for rate limits (429) or server errors (5xx) to trigger a retry
|
| 94 |
if response.status_code in (429, 500, 502, 503, 504):
|
| 95 |
response.raise_for_status()
|
|
|
|
| 103 |
refusals_str = None
|
| 104 |
refusal_rate = None
|
| 105 |
initial_refusals_str = None
|
| 106 |
+
|
| 107 |
# Download the README.md via raw URL for high-speed fetching
|
| 108 |
url = f"https://huggingface.co/{model_id}/raw/main/README.md"
|
| 109 |
try:
|
| 110 |
response = fetch_url(url)
|
| 111 |
if response.status_code == 200:
|
| 112 |
readme_text = response.text
|
| 113 |
+
|
| 114 |
# Extract KL divergence using Regex
|
| 115 |
# Matches formats like: "KL divergence | 0.0033"
|
| 116 |
kl_match = re.search(r"(?i)KL\s*divergence[^a-zA-Z\d\n]*?([\d\.]+)", readme_text)
|
|
|
|
| 119 |
kl_div = float(kl_match.group(1))
|
| 120 |
except ValueError as e:
|
| 121 |
logging.warning(f"Could not parse KL divergence for {model_id}: {e}")
|
| 122 |
+
|
| 123 |
# Extract Refusals using Regex
|
| 124 |
# Matches formats like: "Refusals | 15/100" or "15 / 100"
|
| 125 |
for line in readme_text.split('\n'):
|
|
|
|
| 128 |
fractions = re.findall(r"(\d+)\s*/\s*(\d+)", line)
|
| 129 |
if not fractions:
|
| 130 |
continue
|
| 131 |
+
|
| 132 |
if 'initial' in line_lower and initial_refusals_str is None:
|
| 133 |
initial_refusals_str = f"{fractions[0][0]}/{fractions[0][1]}"
|
| 134 |
elif 'initial' not in line_lower and refusals_str is None:
|
| 135 |
refusals_str = f"{fractions[0][0]}/{fractions[0][1]}"
|
| 136 |
try:
|
| 137 |
+
refusal_rate = (int(fractions[0][0]) / int(fractions[0][1])) * 100
|
| 138 |
except (ValueError, ZeroDivisionError) as e:
|
| 139 |
logging.warning(f"Could not parse refusals for {model_id}: {e}")
|
| 140 |
continue
|
| 141 |
if len(fractions) >= 2 and initial_refusals_str is None:
|
| 142 |
initial_refusals_str = f"{fractions[1][0]}/{fractions[1][1]}"
|
| 143 |
+
|
| 144 |
except requests.exceptions.RequestException as e:
|
| 145 |
logging.warning(f"Error fetching README for {model_id}: {e}")
|
| 146 |
except Exception as e:
|
|
|
|
| 161 |
"URL": f"https://huggingface.co/{model_id}"
|
| 162 |
}
|
| 163 |
|
| 164 |
+
# Check for reproducibility record first and then fallback to parsing data from readme.md
|
| 165 |
def get_model_data(model):
|
| 166 |
model_id = model.id
|
| 167 |
url = f"https://huggingface.co/{model_id}/raw/main/reproduce/reproduce.json"
|
|
|
|
| 197 |
logging.error(f"Unexpected error processing JSON for {model_id}: {e}")
|
| 198 |
return fetch_model_info(model)
|
| 199 |
|
| 200 |
+
# ---------------------------------------------------------------------------
|
| 201 |
+
# Two-stage cached loading (faster initial load)
|
| 202 |
+
# ---------------------------------------------------------------------------
|
| 203 |
+
@st.cache_data(ttl=3600, show_spinner=False)
|
| 204 |
+
def list_heretic_metadata():
|
| 205 |
+
"""Stage 1: HfApi only, no per-model fetch. Cheap and fast.
|
| 206 |
+
|
| 207 |
+
Returns ALL models (including quants) with Created/LastModified dates
|
| 208 |
+
so the caller can pre-filter by date/query BEFORE the expensive
|
| 209 |
+
per-model README/reproduce.json parsing.
|
| 210 |
+
"""
|
| 211 |
api = HfApi()
|
|
|
|
| 212 |
# Query all models using the Hugging Face Hub `filter` parameter
|
| 213 |
models = list(api.list_models(filter="heretic"))
|
| 214 |
+
out = []
|
| 215 |
+
for m in models:
|
| 216 |
+
mid = m.id
|
| 217 |
+
created = getattr(m, "created_at", None)
|
| 218 |
+
modified = getattr(m, "last_modified", None)
|
| 219 |
+
# Normalise to ISO strings (cache-safe, JSON-safe)
|
| 220 |
+
try:
|
| 221 |
+
created_iso = created.isoformat() if hasattr(created, "isoformat") else (str(created) if created else None)
|
| 222 |
+
except Exception:
|
| 223 |
+
created_iso = None
|
| 224 |
+
try:
|
| 225 |
+
modified_iso = modified.isoformat() if hasattr(modified, "isoformat") else (str(modified) if modified else None)
|
| 226 |
+
except Exception:
|
| 227 |
+
modified_iso = None
|
| 228 |
+
out.append({
|
| 229 |
+
"Model ID": mid,
|
| 230 |
+
"Likes": getattr(m, "likes", 0) or 0,
|
| 231 |
+
"Downloads": getattr(m, "downloads", 0) or 0,
|
| 232 |
+
"URL": f"https://huggingface.co/{mid}",
|
| 233 |
+
"Created": created_iso,
|
| 234 |
+
"LastModified": modified_iso,
|
| 235 |
+
})
|
| 236 |
+
return out
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@st.cache_data(ttl=3600, show_spinner=False)
|
| 240 |
+
def cached_get_model_data(model_id: str, likes: int = 0, downloads: int = 0):
|
| 241 |
+
"""Stage 2: per-model reasoning cache, keyed by model_id only.
|
| 242 |
+
|
| 243 |
+
likes/downloads are accepted for back-compat but IGNORED in the key path:
|
| 244 |
+
popularity is merged later via popularity_store so a like-spike doesn't
|
| 245 |
+
invalidate the expensive KL/refusal parse. Pass model_id alone going forward.
|
| 246 |
+
"""
|
| 247 |
+
proxy = SimpleNamespace(id=model_id, likes=0, downloads=0)
|
| 248 |
+
return get_model_data(proxy)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
@st.cache_data(ttl=3600, show_spinner=False) # Cache for 1 hour, back-compat shim
|
| 252 |
+
def get_heretic_models():
|
| 253 |
+
"""Fetches all heretic models and their metrics concurrently."""
|
| 254 |
+
meta = list_heretic_metadata()
|
| 255 |
data = []
|
| 256 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
|
| 257 |
+
results = executor.map(
|
| 258 |
+
lambda d: cached_get_model_data(d["Model ID"], d["Likes"], d["Downloads"]),
|
| 259 |
+
meta,
|
| 260 |
+
)
|
| 261 |
for res in results:
|
| 262 |
if res is not None:
|
| 263 |
data.append(res)
|
|
|
|
| 264 |
return data
|
| 265 |
|
| 266 |
+
# ---------------------------------------------------------------------------
|
| 267 |
+
# Filtering (pure, testable)
|
| 268 |
+
# ---------------------------------------------------------------------------
|
| 269 |
+
def _model_id_series(df: pd.DataFrame) -> pd.Series:
|
| 270 |
+
col = "Model ID" if "Model ID" in df.columns else "Model"
|
| 271 |
+
if col in df.columns:
|
| 272 |
+
return df[col].astype("string")
|
| 273 |
+
return pd.Series([""] * len(df), index=df.index, dtype="string")
|
| 274 |
|
| 275 |
+
|
| 276 |
+
def _parse_created(v):
|
| 277 |
+
"""Parse Created ISO string to aware datetime, or None."""
|
| 278 |
+
if v is None or (isinstance(v, float) and pd.isna(v)):
|
| 279 |
+
return None
|
| 280 |
+
if isinstance(v, datetime):
|
| 281 |
+
dt = v
|
| 282 |
+
else:
|
| 283 |
+
try:
|
| 284 |
+
dt = datetime.fromisoformat(str(v).replace("Z", "+00:00"))
|
| 285 |
+
except Exception:
|
| 286 |
+
return None
|
| 287 |
+
if dt is not None and dt.tzinfo is None:
|
| 288 |
+
dt = dt.replace(tzinfo=timezone.utc)
|
| 289 |
+
return dt
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def filter_metadata_by_date(metadata: list, date_range_key: str, unknowns_key: str = "Include unknowns") -> list:
|
| 293 |
+
"""Pre-filter raw metadata list by Created date BEFORE expensive parsing.
|
| 294 |
+
|
| 295 |
+
Unknowns handling is now an explicit user choice (not silently kept):
|
| 296 |
+
- "Include unknowns": keep models with missing Created date (old default).
|
| 297 |
+
- "Hide unknowns": drop them β strictest, smallest parse set.
|
| 298 |
+
- "Only unknowns": parse ONLY models with missing dates (audit mode).
|
| 299 |
+
"""
|
| 300 |
+
days = DATE_OPTIONS.get(date_range_key, None)
|
| 301 |
+
if unknowns_key not in UNKNOWN_OPTIONS:
|
| 302 |
+
unknowns_key = "Include unknowns"
|
| 303 |
+
if days is None and unknowns_key == "Include unknowns":
|
| 304 |
+
return list(metadata)
|
| 305 |
+
cutoff = None
|
| 306 |
+
if days is not None:
|
| 307 |
+
cutoff = datetime.now(timezone.utc) - pd.Timedelta(days=days).to_pytimedelta()
|
| 308 |
+
kept = []
|
| 309 |
+
for d in metadata:
|
| 310 |
+
dt = _parse_created(d.get("Created"))
|
| 311 |
+
if dt is None:
|
| 312 |
+
if unknowns_key in ("Include unknowns", "Only unknowns"):
|
| 313 |
+
kept.append(d)
|
| 314 |
+
elif cutoff is None:
|
| 315 |
+
# All time + Hide unknowns -> drop unknowns (already skipped), keep dated
|
| 316 |
+
kept.append(d)
|
| 317 |
+
elif unknowns_key == "Only unknowns":
|
| 318 |
+
continue # dated rows excluded in audit mode
|
| 319 |
+
elif dt >= cutoff:
|
| 320 |
+
kept.append(d)
|
| 321 |
+
return kept
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def prefilter_metadata(metadata: list, query: str, show_quants: bool, date_range_key: str, unknowns_key: str = "Include unknowns") -> list:
|
| 325 |
+
"""Cheap pre-fetch filter: date (+unknowns handling) + quant + query. No network."""
|
| 326 |
+
out = filter_metadata_by_date(metadata, date_range_key, unknowns_key)
|
| 327 |
+
if not show_quants:
|
| 328 |
+
out = [d for d in out if not QUANT_PATTERNS.search(d.get("Model ID", ""))]
|
| 329 |
+
if query is not None:
|
| 330 |
+
q = str(query).strip().lower()
|
| 331 |
+
if q:
|
| 332 |
+
out = [d for d in out if q in str(d.get("Model ID", "")).lower()]
|
| 333 |
+
return out
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def apply_filters(df, query, kl_range, refusal_range,
|
| 337 |
+
min_likes, min_downloads, show_quants, date_range="All time", unknowns="Include unknowns"):
|
| 338 |
+
"""Pure filter. Never mutates input. Handles empty df, NaNs, case-insensitive query."""
|
| 339 |
+
if df is None or len(df) == 0:
|
| 340 |
+
return df.copy() if isinstance(df, pd.DataFrame) else pd.DataFrame()
|
| 341 |
+
mask = pd.Series(True, index=df.index)
|
| 342 |
+
if query is not None:
|
| 343 |
+
q = str(query).strip()
|
| 344 |
+
if q:
|
| 345 |
+
ids = _model_id_series(df).fillna("")
|
| 346 |
+
mask &= ids.str.contains(q, case=False, na=False, regex=False)
|
| 347 |
+
if kl_range is not None and "KL Divergence" in df.columns:
|
| 348 |
+
try:
|
| 349 |
+
lo, hi = float(kl_range[0]), float(kl_range[1])
|
| 350 |
+
lo, hi = (hi, lo) if lo > hi else (lo, hi)
|
| 351 |
+
kl = pd.to_numeric(df["KL Divergence"], errors="coerce")
|
| 352 |
+
mask &= kl.between(lo, hi, inclusive="both")
|
| 353 |
+
except (TypeError, ValueError, IndexError):
|
| 354 |
+
pass
|
| 355 |
+
if refusal_range is not None and "Refusal Rate (%)" in df.columns:
|
| 356 |
+
try:
|
| 357 |
+
lo, hi = float(refusal_range[0]), float(refusal_range[1])
|
| 358 |
+
lo, hi = (hi, lo) if lo > hi else (lo, hi)
|
| 359 |
+
rr = pd.to_numeric(df["Refusal Rate (%)"], errors="coerce")
|
| 360 |
+
mask &= rr.between(lo, hi, inclusive="both")
|
| 361 |
+
except (TypeError, ValueError, IndexError):
|
| 362 |
+
pass
|
| 363 |
+
for col, thresh in (("Likes", min_likes), ("Downloads", min_downloads)):
|
| 364 |
+
if col in df.columns and thresh is not None:
|
| 365 |
+
try:
|
| 366 |
+
t = float(thresh)
|
| 367 |
+
except (TypeError, ValueError):
|
| 368 |
+
continue
|
| 369 |
+
if pd.isna(t) or t <= 0:
|
| 370 |
+
continue
|
| 371 |
+
vals = pd.to_numeric(df[col], errors="coerce").fillna(0)
|
| 372 |
+
mask &= vals >= t
|
| 373 |
+
if not show_quants:
|
| 374 |
+
ids = _model_id_series(df).fillna("")
|
| 375 |
+
mask &= ~ids.str.contains(QUANT_RE, case=False, na=False, regex=True)
|
| 376 |
+
# Date filter on Created column (post-fetch safety net; main saving is pre-fetch)
|
| 377 |
+
days = DATE_OPTIONS.get(date_range, None)
|
| 378 |
+
if unknowns not in UNKNOWN_OPTIONS:
|
| 379 |
+
unknowns = "Include unknowns"
|
| 380 |
+
if "Created" in df.columns and (days is not None or unknowns != "Include unknowns"):
|
| 381 |
+
try:
|
| 382 |
+
cutoff = None
|
| 383 |
+
if days is not None:
|
| 384 |
+
cutoff = datetime.now(timezone.utc) - pd.Timedelta(days=days).to_pytimedelta()
|
| 385 |
+
parsed = df["Created"].apply(_parse_created)
|
| 386 |
+
if unknowns == "Only unknowns":
|
| 387 |
+
date_mask = parsed.apply(lambda dt: dt is None)
|
| 388 |
+
elif unknowns == "Hide unknowns":
|
| 389 |
+
if cutoff is None:
|
| 390 |
+
date_mask = parsed.apply(lambda dt: dt is not None)
|
| 391 |
+
else:
|
| 392 |
+
date_mask = parsed.apply(lambda dt: dt is not None and dt >= cutoff)
|
| 393 |
+
else: # Include unknowns
|
| 394 |
+
if cutoff is None:
|
| 395 |
+
date_mask = pd.Series(True, index=df.index)
|
| 396 |
+
else:
|
| 397 |
+
date_mask = parsed.apply(lambda dt: True if dt is None else dt >= cutoff)
|
| 398 |
+
mask &= date_mask.fillna(True).to_numpy() if hasattr(date_mask, "fillna") else date_mask
|
| 399 |
+
except Exception:
|
| 400 |
+
pass
|
| 401 |
+
return df.loc[mask].copy().reset_index(drop=True)
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def _qp_first(key, default=""):
|
| 405 |
+
v = st.query_params.get(key, default)
|
| 406 |
+
return v[0] if isinstance(v, list) and v else v
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def _to_float(v, default):
|
| 410 |
+
try:
|
| 411 |
+
f = float(v)
|
| 412 |
+
return f if not pd.isna(f) else default
|
| 413 |
+
except (TypeError, ValueError):
|
| 414 |
+
return default
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def _to_int(v, default):
|
| 418 |
+
try:
|
| 419 |
+
return max(0, int(float(v)))
|
| 420 |
+
except (TypeError, ValueError):
|
| 421 |
+
return default
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def init_filter_state_from_query_params():
|
| 425 |
+
if st.session_state.get("_qp_init"):
|
| 426 |
+
return
|
| 427 |
+
d = FILTER_DEFAULTS
|
| 428 |
+
kl_lo = _to_float(_qp_first("kl_min", d["kl_range"][0]), d["kl_range"][0])
|
| 429 |
+
kl_hi = _to_float(_qp_first("kl_max", d["kl_range"][1]), d["kl_range"][1])
|
| 430 |
+
rr_lo = _to_float(_qp_first("rr_min", d["refusal_range"][0]), d["refusal_range"][0])
|
| 431 |
+
rr_hi = _to_float(_qp_first("rr_max", d["refusal_range"][1]), d["refusal_range"][1])
|
| 432 |
+
st.session_state.setdefault("query", str(_qp_first("q", d["query"])))
|
| 433 |
+
_dr = str(_qp_first("date_range", d["date_range"]))
|
| 434 |
+
st.session_state.setdefault("date_range", _dr if _dr in DATE_OPTIONS else d["date_range"])
|
| 435 |
+
_unk = str(_qp_first("unknowns", d["unknowns"]))
|
| 436 |
+
st.session_state.setdefault("unknowns", _unk if _unk in UNKNOWN_OPTIONS else d["unknowns"])
|
| 437 |
+
st.session_state.setdefault("kl_range", (min(kl_lo, kl_hi), max(kl_lo, kl_hi)))
|
| 438 |
+
st.session_state.setdefault("refusal_range", (min(rr_lo, rr_hi), max(rr_lo, rr_hi)))
|
| 439 |
+
st.session_state.setdefault("min_likes", _to_int(_qp_first("min_likes", d["min_likes"]), d["min_likes"]))
|
| 440 |
+
st.session_state.setdefault("min_downloads", _to_int(_qp_first("min_downloads", d["min_downloads"]), d["min_downloads"]))
|
| 441 |
+
st.session_state.setdefault("show_quants", str(_qp_first("show_quants", "")).lower() in ("1", "true", "yes"))
|
| 442 |
+
sort = str(_qp_first("sort_by", d["sort_by"]))
|
| 443 |
+
st.session_state.setdefault("sort_by", sort if sort in SORT_OPTIONS else d["sort_by"])
|
| 444 |
+
st.session_state["_qp_init"] = True
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def sync_filter_state_to_query_params():
|
| 448 |
+
st.query_params["q"] = st.session_state.get("query", "")
|
| 449 |
+
st.query_params["date_range"] = str(st.session_state.get("date_range", FILTER_DEFAULTS["date_range"]))
|
| 450 |
+
st.query_params["unknowns"] = str(st.session_state.get("unknowns", FILTER_DEFAULTS["unknowns"]))
|
| 451 |
+
kl = st.session_state.get("kl_range", FILTER_DEFAULTS["kl_range"])
|
| 452 |
+
rr = st.session_state.get("refusal_range", FILTER_DEFAULTS["refusal_range"])
|
| 453 |
+
st.query_params["kl_min"] = str(float(kl[0]))
|
| 454 |
+
st.query_params["kl_max"] = str(float(kl[1]))
|
| 455 |
+
st.query_params["rr_min"] = str(float(rr[0]))
|
| 456 |
+
st.query_params["rr_max"] = str(float(rr[1]))
|
| 457 |
+
st.query_params["min_likes"] = str(int(st.session_state.get("min_likes", 0) or 0))
|
| 458 |
+
st.query_params["min_downloads"] = str(int(st.session_state.get("min_downloads", 0) or 0))
|
| 459 |
+
st.query_params["show_quants"] = "1" if st.session_state.get("show_quants") else "0"
|
| 460 |
+
st.query_params["sort_by"] = str(st.session_state.get("sort_by", FILTER_DEFAULTS["sort_by"]))
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def _short(url: str) -> str:
|
| 464 |
+
s = str(url)
|
| 465 |
+
return s.split("huggingface.co/")[-1] if "huggingface.co/" in s else s
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def _parse_fraction(s) -> tuple:
|
| 469 |
+
"""Parse '15/100' β (15, 100). Returns (None, None) on failure."""
|
| 470 |
+
if s is None or (isinstance(s, float) and pd.isna(s)):
|
| 471 |
+
return (None, None)
|
| 472 |
+
m = re.search(r"(\d+)\s*/\s*(\d+)", str(s))
|
| 473 |
+
if not m:
|
| 474 |
+
return (None, None)
|
| 475 |
+
try:
|
| 476 |
+
return (int(m.group(1)), int(m.group(2)))
|
| 477 |
+
except (ValueError, ZeroDivisionError):
|
| 478 |
+
return (None, None)
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
def _kl_band(kl) -> str:
|
| 482 |
+
"""Rough weight-impact bands. Not a verdict β higher KL = more brain drift."""
|
| 483 |
+
if kl is None or (isinstance(kl, float) and pd.isna(kl)):
|
| 484 |
+
return "unknown"
|
| 485 |
+
try:
|
| 486 |
+
kl = float(kl)
|
| 487 |
+
except (TypeError, ValueError):
|
| 488 |
+
return "unknown"
|
| 489 |
+
if kl < 0.01:
|
| 490 |
+
return "minimal drift"
|
| 491 |
+
if kl < 0.05:
|
| 492 |
+
return "moderate drift"
|
| 493 |
+
if kl < 0.15:
|
| 494 |
+
return "high drift"
|
| 495 |
+
return "very high drift"
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def _compare_note(kl, ref_num, ref_den, init_num, init_den) -> str:
|
| 499 |
+
"""One-line grain-of-salt note per model. No trophies."""
|
| 500 |
+
parts = []
|
| 501 |
+
# Sample-size caution
|
| 502 |
+
if ref_den is None:
|
| 503 |
+
parts.append("unknown sample size β weak evidence")
|
| 504 |
+
elif ref_den < 50:
|
| 505 |
+
parts.append(f"small sample (n={ref_den}) β take with grain of salt")
|
| 506 |
+
# Drop vs base
|
| 507 |
+
if init_num is not None and ref_num is not None and init_den and ref_den:
|
| 508 |
+
drop = init_num - ref_num
|
| 509 |
+
parts.append(f"drop {drop} (base {init_num}/{init_den} β {ref_num}/{ref_den})")
|
| 510 |
+
# KL vs refusal tradeoff β the core complaint: 0% refusal + high KL = stupid
|
| 511 |
+
try:
|
| 512 |
+
klf = float(kl) if kl is not None else None
|
| 513 |
+
except (TypeError, ValueError):
|
| 514 |
+
klf = None
|
| 515 |
+
try:
|
| 516 |
+
rate = (ref_num / ref_den * 100) if ref_num is not None and ref_den else None
|
| 517 |
+
except ZeroDivisionError:
|
| 518 |
+
rate = None
|
| 519 |
+
if rate is not None and klf is not None:
|
| 520 |
+
if rate == 0 and klf > 0.1:
|
| 521 |
+
parts.append("0 refusals BUT high KL β possibly lobotomized, check reasoning")
|
| 522 |
+
elif rate == 0 and klf > 0.05:
|
| 523 |
+
parts.append("0 refusals with notable drift β verify quality")
|
| 524 |
+
elif rate <= 5 and klf is not None and klf < 0.02:
|
| 525 |
+
parts.append("good balance: low refusal + low drift")
|
| 526 |
+
elif rate > 20:
|
| 527 |
+
parts.append("still refusal-prone, regardless of KL")
|
| 528 |
+
elif klf is not None and klf > 0.1:
|
| 529 |
+
parts.append("high KL β expect reasoning damage")
|
| 530 |
+
return "; ".join(parts) if parts else "β"
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
# ---------------------------------------------------------------------------
|
| 534 |
+
# Main execution: metadata instantly, pre-filter by date, then parse subset
|
| 535 |
+
# ---------------------------------------------------------------------------
|
| 536 |
+
init_filter_state_from_query_params()
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def _reset_filters():
|
| 540 |
+
"""Callback for Reset button β runs BEFORE widgets instantiate, so no
|
| 541 |
+
StreamlitWidgetAlreadyInstantiatedError (the crash you saw)."""
|
| 542 |
+
for k, v in FILTER_DEFAULTS.items():
|
| 543 |
+
st.session_state[k] = v
|
| 544 |
+
st.query_params.clear()
|
| 545 |
+
st.session_state.metrics_done = False
|
| 546 |
+
st.session_state.metrics_rows = []
|
| 547 |
+
st.session_state.metrics_signature = None
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
if "metrics_done" not in st.session_state:
|
| 551 |
+
st.session_state.metrics_done = False
|
| 552 |
+
if "metrics_rows" not in st.session_state:
|
| 553 |
+
st.session_state.metrics_rows = []
|
| 554 |
+
if "metrics_signature" not in st.session_state:
|
| 555 |
+
st.session_state.metrics_signature = None
|
| 556 |
+
|
| 557 |
+
# --- EARLY pre-filters: these decide HOW MANY models get parsed ---
|
| 558 |
+
# Shown before any expensive README/reproduce.json fetching.
|
| 559 |
+
st.sidebar.header("Load scope (saves parsing time)")
|
| 560 |
+
early_date = st.sidebar.selectbox(
|
| 561 |
+
"Release date",
|
| 562 |
+
list(DATE_OPTIONS.keys()),
|
| 563 |
+
key="date_range",
|
| 564 |
+
help="Pre-filters by Hugging Face creation date BEFORE parsing. Pick a recent window to avoid parsing ~2000 models.",
|
| 565 |
+
)
|
| 566 |
+
early_unknowns = st.sidebar.selectbox(
|
| 567 |
+
"Unknown dates",
|
| 568 |
+
list(UNKNOWN_OPTIONS),
|
| 569 |
+
key="unknowns",
|
| 570 |
+
help="Models with missing HF creation date. Include (old default), Hide (strictest, smallest parse), or Only (audit them).",
|
| 571 |
+
)
|
| 572 |
+
early_query = st.sidebar.text_input(
|
| 573 |
+
"Search model ID (pre-filter)",
|
| 574 |
+
key="query",
|
| 575 |
+
placeholder="e.g. llama-3 heretic β¦",
|
| 576 |
+
help="Case-insensitive substring. Applied BEFORE parsing to skip non-matching models.",
|
| 577 |
+
)
|
| 578 |
+
early_quants = st.sidebar.checkbox(
|
| 579 |
+
"Show quantised models",
|
| 580 |
+
key="show_quants",
|
| 581 |
+
help="Untick to hide GGUF/MLX/AWQ/GPTQ/EXL2/EXL3/quant/INT8/INT4/OQ/MXFP4/MXFP8 before parsing.",
|
| 582 |
+
)
|
| 583 |
+
try:
|
| 584 |
+
from src.popularity_store import load_popularity, save_popularity, snapshot_from_metadata
|
| 585 |
+
except ImportError:
|
| 586 |
+
try:
|
| 587 |
+
from popularity_store import load_popularity, save_popularity, snapshot_from_metadata
|
| 588 |
+
except ImportError:
|
| 589 |
+
load_popularity = lambda: {} # noqa: E731
|
| 590 |
+
save_popularity = lambda m: "" # noqa: E731
|
| 591 |
+
snapshot_from_metadata = lambda md: {} # noqa: E731
|
| 592 |
+
with st.sidebar.expander("Popularity (likes/downloads)", expanded=False):
|
| 593 |
+
st.caption("Stored separately in data/popularity.json β refreshable without re-parsing READMEs.")
|
| 594 |
+
if st.button("π Refresh popularity file", use_container_width=True):
|
| 595 |
+
try:
|
| 596 |
+
_live = list_heretic_metadata()
|
| 597 |
+
_path = save_popularity(snapshot_from_metadata(_live))
|
| 598 |
+
st.success(f"Saved {len(_live)} entries to {_path or 'data/popularity.json'}")
|
| 599 |
+
except Exception as e:
|
| 600 |
+
st.error(f"Could not save popularity: {e}")
|
| 601 |
+
try:
|
| 602 |
+
_pop = load_popularity()
|
| 603 |
+
st.caption(f"{len(_pop)} models in popularity file.")
|
| 604 |
+
except Exception:
|
| 605 |
+
pass
|
| 606 |
+
|
| 607 |
+
col_a, col_b = st.columns([1, 4])
|
| 608 |
+
with col_a:
|
| 609 |
+
refresh = st.button("π Refresh metrics", use_container_width=True)
|
| 610 |
+
with col_b:
|
| 611 |
+
auto = st.checkbox("Auto-load metrics", value=True)
|
| 612 |
+
|
| 613 |
+
try:
|
| 614 |
+
with st.spinner("Fetching model list (fast metadata)..."):
|
| 615 |
+
metadata = list_heretic_metadata()
|
| 616 |
+
except Exception as e:
|
| 617 |
+
st.error(f"Could not list heretic models: {e}")
|
| 618 |
+
st.stop()
|
| 619 |
+
|
| 620 |
+
# Cheap pre-filter BEFORE expensive parsing β this is the load-time win.
|
| 621 |
+
target_meta = prefilter_metadata(metadata, early_query, early_quants, early_date, early_unknowns)
|
| 622 |
+
st.caption(
|
| 623 |
+
f"Found {len(metadata)} models (metadata cached 1h). "
|
| 624 |
+
f"Scope '{early_date}' + '{early_unknowns}' + search β **{len(target_meta)} to parse**."
|
| 625 |
+
)
|
| 626 |
+
if len(target_meta) == 0:
|
| 627 |
+
st.info("No models in this date/search scope β widen the date range, set Unknowns to Include, or clear search.")
|
| 628 |
+
st.stop()
|
| 629 |
+
if len(target_meta) > 500:
|
| 630 |
+
st.warning(
|
| 631 |
+
f"Scope selects {len(target_meta)} models β parsing will take a while. "
|
| 632 |
+
"Tip: pick 'Last month' / 'Last week', Hide unknowns, or add a search term."
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
fetch_signature = (early_date, early_unknowns, early_query.strip().lower() if early_query else "", bool(early_quants))
|
| 636 |
+
if st.session_state.metrics_signature is not None and st.session_state.metrics_signature != fetch_signature:
|
| 637 |
+
# Scope changed β old parsed rows no longer valid
|
| 638 |
+
st.session_state.metrics_done = False
|
| 639 |
+
st.session_state.metrics_rows = []
|
| 640 |
+
|
| 641 |
+
should_run = (auto and not st.session_state.metrics_done) or refresh
|
| 642 |
+
if refresh:
|
| 643 |
+
st.session_state.metrics_done = False
|
| 644 |
+
st.session_state.metrics_rows = []
|
| 645 |
+
|
| 646 |
+
if st.session_state.metrics_done and st.session_state.metrics_signature == fetch_signature:
|
| 647 |
+
models_data = st.session_state.metrics_rows
|
| 648 |
+
elif should_run:
|
| 649 |
+
status_slot = st.empty()
|
| 650 |
+
progress = st.progress(0)
|
| 651 |
+
rows = []
|
| 652 |
+
with status_slot.status("Fetching reproduce.json β README fallback...", expanded=False) as status:
|
| 653 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as ex:
|
| 654 |
+
fut_to_id = {
|
| 655 |
+
ex.submit(cached_get_model_data, d["Model ID"], d["Likes"], d["Downloads"]): d["Model ID"]
|
| 656 |
+
for d in target_meta
|
| 657 |
+
}
|
| 658 |
+
done = 0
|
| 659 |
+
for fut in concurrent.futures.as_completed(fut_to_id):
|
| 660 |
+
try:
|
| 661 |
+
res = fut.result()
|
| 662 |
+
if res is not None:
|
| 663 |
+
rows.append(res)
|
| 664 |
+
except Exception as e:
|
| 665 |
+
logging.warning(f"Worker failed for {fut_to_id[fut]}: {e}")
|
| 666 |
+
done += 1
|
| 667 |
+
progress.progress(done / max(len(target_meta), 1))
|
| 668 |
+
status.update(label=f"Parsed {done}/{len(target_meta)} β {len(rows)} with metrics...")
|
| 669 |
+
status.update(label=f"Done: {len(rows)}/{len(target_meta)} with metrics", state="complete")
|
| 670 |
+
# Merge Created dates back in for display/filtering + popularity
|
| 671 |
+
# (likes/downloads live in popularity_store, NOT in the reasoning cache key)
|
| 672 |
+
created_map = {d["Model ID"]: d.get("Created") for d in target_meta}
|
| 673 |
+
meta_like_map = {d["Model ID"]: d for d in target_meta}
|
| 674 |
+
try:
|
| 675 |
+
_file_map = load_popularity()
|
| 676 |
+
except Exception:
|
| 677 |
+
_file_map = {}
|
| 678 |
+
for r in rows:
|
| 679 |
+
r.setdefault("Created", created_map.get(r.get("Model ID")))
|
| 680 |
+
mid = r.get("Model ID")
|
| 681 |
+
file_hit = (_file_map or {}).get(mid, {}) if mid else {}
|
| 682 |
+
meta_hit = meta_like_map.get(mid, {}) if mid else {}
|
| 683 |
+
if isinstance(file_hit, dict) and ("likes" in file_hit or "downloads" in file_hit):
|
| 684 |
+
try:
|
| 685 |
+
r["Likes"] = int(file_hit.get("likes", r.get("Likes", 0)) or 0)
|
| 686 |
+
except (TypeError, ValueError):
|
| 687 |
+
pass
|
| 688 |
+
try:
|
| 689 |
+
r["Downloads"] = int(file_hit.get("downloads", r.get("Downloads", 0)) or 0)
|
| 690 |
+
except (TypeError, ValueError):
|
| 691 |
+
pass
|
| 692 |
+
elif mid in meta_like_map:
|
| 693 |
+
try:
|
| 694 |
+
r["Likes"] = int(meta_hit.get("Likes", r.get("Likes", 0)) or 0)
|
| 695 |
+
except (TypeError, ValueError):
|
| 696 |
+
pass
|
| 697 |
+
try:
|
| 698 |
+
r["Downloads"] = int(meta_hit.get("Downloads", r.get("Downloads", 0)) or 0)
|
| 699 |
+
except (TypeError, ValueError):
|
| 700 |
+
pass
|
| 701 |
+
st.session_state.metrics_rows = rows
|
| 702 |
+
st.session_state.metrics_done = True
|
| 703 |
+
st.session_state.metrics_signature = fetch_signature
|
| 704 |
+
progress.empty()
|
| 705 |
+
status_slot.empty()
|
| 706 |
+
models_data = rows
|
| 707 |
+
st.rerun()
|
| 708 |
else:
|
| 709 |
+
st.info("Tick Auto-load or press Refresh to fetch KL/Refusals.")
|
| 710 |
+
st.stop()
|
| 711 |
+
|
| 712 |
+
if not models_data:
|
| 713 |
+
st.warning("No models found with the 'heretic' tag in this scope.")
|
| 714 |
+
st.stop()
|
| 715 |
+
|
| 716 |
+
df = pd.DataFrame(models_data)
|
| 717 |
+
# Make Model ID a clickable link
|
| 718 |
+
df["Model"] = df["URL"]
|
| 719 |
+
# Select and order columns for display (Created kept for filtering, shown in table too)
|
| 720 |
+
cols = ["Model", "KL Divergence", "Initial Refusals", "Refusals", "Refusal Rate (%)", "Likes", "Downloads"]
|
| 721 |
+
if "Created" in df.columns:
|
| 722 |
+
cols.append("Created")
|
| 723 |
+
display_df = df[cols]
|
| 724 |
+
|
| 725 |
+
# --- LATE metric filters (need parsed KL/Refusals) ---
|
| 726 |
+
# NOTE: no writes to st.session_state widget keys here β that was the
|
| 727 |
+
# StreamlitWidgetAlreadyInstantiatedError. Slider max is extended to fit the
|
| 728 |
+
# stored value instead of writing back.
|
| 729 |
+
kl_max_data = float(pd.to_numeric(display_df["KL Divergence"], errors="coerce").max() or 0.5)
|
| 730 |
+
_cur_kl = st.session_state.get("kl_range", FILTER_DEFAULTS["kl_range"])
|
| 731 |
+
try:
|
| 732 |
+
_cur_hi = float(_cur_kl[1])
|
| 733 |
+
except Exception:
|
| 734 |
+
_cur_hi = 0.5
|
| 735 |
+
kl_slider_max = max(0.5, kl_max_data, _cur_hi)
|
| 736 |
+
|
| 737 |
+
toolbar_sort, toolbar_reset = st.columns([3, 1])
|
| 738 |
+
with toolbar_sort:
|
| 739 |
+
sort_by = st.selectbox("Sort by", list(SORT_OPTIONS.keys()), key="sort_by")
|
| 740 |
+
with toolbar_reset:
|
| 741 |
+
st.write("")
|
| 742 |
+
st.button("Reset filters", key="reset_filters", use_container_width=True, on_click=_reset_filters)
|
| 743 |
+
|
| 744 |
+
with st.sidebar:
|
| 745 |
+
st.header("Metric filters")
|
| 746 |
+
kl_range = st.slider(
|
| 747 |
+
"KL Divergence",
|
| 748 |
+
min_value=0.0, max_value=float(kl_slider_max), step=0.001, format="%.3f",
|
| 749 |
+
key="kl_range",
|
| 750 |
)
|
| 751 |
+
refusal_range = st.slider(
|
| 752 |
+
"Refusal Rate (%)",
|
| 753 |
+
min_value=0.0, max_value=100.0, step=0.5, format="%.1f",
|
| 754 |
+
key="refusal_range",
|
| 755 |
+
)
|
| 756 |
+
min_likes = st.number_input("Min Likes", min_value=0, step=1, key="min_likes")
|
| 757 |
+
min_downloads = st.number_input("Min Downloads", min_value=0, step=10, key="min_downloads")
|
| 758 |
+
|
| 759 |
+
sync_filter_state_to_query_params()
|
| 760 |
+
|
| 761 |
+
query = st.session_state.get("query", "")
|
| 762 |
+
show_quants = st.session_state.get("show_quants", False)
|
| 763 |
+
date_range = st.session_state.get("date_range", "All time")
|
| 764 |
+
unknowns = st.session_state.get("unknowns", "Include unknowns")
|
| 765 |
+
filtered_df = apply_filters(display_df, query, kl_range, refusal_range, min_likes, min_downloads, show_quants, date_range, unknowns)
|
| 766 |
+
sort_col, ascending = SORT_OPTIONS.get(sort_by, ("KL Divergence", True))
|
| 767 |
+
if sort_col in filtered_df.columns:
|
| 768 |
+
filtered_df = filtered_df.sort_values(sort_col, ascending=ascending, kind="mergesort").reset_index(drop=True)
|
| 769 |
+
|
| 770 |
+
st.markdown(f"**Showing {len(filtered_df)} of {len(display_df)} models.**")
|
| 771 |
+
|
| 772 |
+
try:
|
| 773 |
+
import altair as alt
|
| 774 |
+
_ALT_OK = True
|
| 775 |
+
except ImportError:
|
| 776 |
+
alt = None # type: ignore
|
| 777 |
+
_ALT_OK = False
|
| 778 |
+
|
| 779 |
+
tab_table, tab_compare, tab_charts = st.tabs(["Table", "Compare", "Charts"])
|
| 780 |
+
|
| 781 |
+
# 1. Table β original config preserved
|
| 782 |
+
with tab_table:
|
| 783 |
+
if filtered_df.empty:
|
| 784 |
+
st.info("No models match the current filter.")
|
| 785 |
+
else:
|
| 786 |
+
st.dataframe(
|
| 787 |
+
filtered_df,
|
| 788 |
+
column_config={
|
| 789 |
+
"Model": st.column_config.LinkColumn("Model", display_text=r"https://huggingface\.co/(.*)"),
|
| 790 |
+
"KL Divergence": st.column_config.NumberColumn("KL Divergence", format="%.4f"),
|
| 791 |
+
"Initial Refusals": st.column_config.TextColumn("Initial Refusals"),
|
| 792 |
+
"Refusals": st.column_config.TextColumn("Refusals"),
|
| 793 |
+
"Refusal Rate (%)": st.column_config.NumberColumn("Refusal Rate (%)", format="%.2f%%"),
|
| 794 |
+
"Likes": st.column_config.NumberColumn("Likes"),
|
| 795 |
+
"Downloads": st.column_config.NumberColumn("Downloads"),
|
| 796 |
+
"Created": st.column_config.TextColumn("Created"),
|
| 797 |
+
},
|
| 798 |
+
hide_index=True,
|
| 799 |
+
width='stretch'
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
# 2. Compare β up to 4, transposed, NO winner trophies
|
| 803 |
+
with tab_compare:
|
| 804 |
+
if filtered_df.empty:
|
| 805 |
+
st.info("No models to compare β adjust filter.")
|
| 806 |
+
else:
|
| 807 |
+
st.caption(
|
| 808 |
+
"Grain of salt: a 0/x refusal alone proves little. "
|
| 809 |
+
"A 30/30 β 0/30 on a tiny sample is weaker than 95/100 β 2/100, "
|
| 810 |
+
"and zero refusals with high KL often means the weights got wrecked β "
|
| 811 |
+
"uncensored but stupid. Read Refusal **with** KL + sample size."
|
| 812 |
+
)
|
| 813 |
+
options = filtered_df["Model"].tolist()
|
| 814 |
+
selected = st.multiselect(
|
| 815 |
+
"Select up to 4 models to compare",
|
| 816 |
+
options=options,
|
| 817 |
+
default=options[:2] if len(options) >= 2 else options,
|
| 818 |
+
max_selections=4,
|
| 819 |
+
format_func=_short,
|
| 820 |
+
)
|
| 821 |
+
if len(selected) < 2:
|
| 822 |
+
st.info("Select at least 2 models to compare.")
|
| 823 |
+
else:
|
| 824 |
+
comp = filtered_df[filtered_df["Model"].isin(selected)].set_index("Model").reindex(selected)
|
| 825 |
+
comp["KL Divergence"] = pd.to_numeric(comp["KL Divergence"], errors="coerce")
|
| 826 |
+
comp["Refusal Rate (%)"] = pd.to_numeric(comp["Refusal Rate (%)"], errors="coerce")
|
| 827 |
+
# Per-model honest summary (no best_kl / best_ref verdicts)
|
| 828 |
+
for m in selected:
|
| 829 |
+
row = comp.loc[m]
|
| 830 |
+
rn, rd = _parse_fraction(row.get("Refusals"))
|
| 831 |
+
inn, ind = _parse_fraction(row.get("Initial Refusals"))
|
| 832 |
+
note = _compare_note(row.get("KL Divergence"), rn, rd, inn, ind)
|
| 833 |
+
band = _kl_band(row.get("KL Divergence"))
|
| 834 |
+
st.info(f"**{_short(m)}** β KL {band} | {note}")
|
| 835 |
+
disp = comp.T.copy()
|
| 836 |
+
disp.columns = [_short(c) for c in disp.columns]
|
| 837 |
+
# Extra derived rows for honest comparison
|
| 838 |
+
try:
|
| 839 |
+
kl_vals = pd.to_numeric(comp["KL Divergence"], errors="coerce")
|
| 840 |
+
disp.loc["KL band"] = [_kl_band(v) for v in kl_vals.reindex(selected).tolist()]
|
| 841 |
+
except Exception:
|
| 842 |
+
pass
|
| 843 |
+
try:
|
| 844 |
+
drops = []
|
| 845 |
+
for m in selected:
|
| 846 |
+
rn, rd = _parse_fraction(comp.loc[m].get("Refusals"))
|
| 847 |
+
inn, ind = _parse_fraction(comp.loc[m].get("Initial Refusals"))
|
| 848 |
+
drops.append(str(inn - rn) if inn is not None and rn is not None else "β")
|
| 849 |
+
disp.loc["Refusal drop (base β final)"] = drops
|
| 850 |
+
except Exception:
|
| 851 |
+
pass
|
| 852 |
+
try:
|
| 853 |
+
ns = []
|
| 854 |
+
for m in selected:
|
| 855 |
+
_, rd = _parse_fraction(comp.loc[m].get("Refusals"))
|
| 856 |
+
ns.append(str(rd) if rd is not None else "β")
|
| 857 |
+
disp.loc["Sample size (n prompts)"] = ns
|
| 858 |
+
except Exception:
|
| 859 |
+
pass
|
| 860 |
+
for metric in list(disp.index):
|
| 861 |
+
for col_url, col_short in zip(selected, disp.columns):
|
| 862 |
+
v = comp.loc[col_url, metric] if metric in comp.columns else disp.loc[metric, col_short]
|
| 863 |
+
if pd.isna(v):
|
| 864 |
+
disp.loc[metric, col_short] = "β"
|
| 865 |
+
elif metric == "KL Divergence":
|
| 866 |
+
try:
|
| 867 |
+
disp.loc[metric, col_short] = f"{float(v):.4f}"
|
| 868 |
+
except (TypeError, ValueError):
|
| 869 |
+
disp.loc[metric, col_short] = str(v)
|
| 870 |
+
elif metric == "Refusal Rate (%)":
|
| 871 |
+
try:
|
| 872 |
+
disp.loc[metric, col_short] = f"{float(v):.2f}%"
|
| 873 |
+
except (TypeError, ValueError):
|
| 874 |
+
disp.loc[metric, col_short] = str(v)
|
| 875 |
+
elif metric in ("Likes", "Downloads"):
|
| 876 |
+
try:
|
| 877 |
+
disp.loc[metric, col_short] = f"{int(float(v)):,}"
|
| 878 |
+
except Exception:
|
| 879 |
+
disp.loc[metric, col_short] = str(v)
|
| 880 |
+
else:
|
| 881 |
+
disp.loc[metric, col_short] = str(v) if v is not None else "β"
|
| 882 |
+
disp.index.name = "Metric"
|
| 883 |
+
st.table(disp)
|
| 884 |
+
|
| 885 |
+
# 3. Charts β altair, NaN/empty safe
|
| 886 |
+
with tab_charts:
|
| 887 |
+
if not _ALT_OK:
|
| 888 |
+
st.warning("`altair` is not installed. Add `altair` to requirements.txt and reboot.")
|
| 889 |
+
elif filtered_df.empty:
|
| 890 |
+
st.info("No data to chart β adjust filter.")
|
| 891 |
+
else:
|
| 892 |
+
chart_df = filtered_df.copy()
|
| 893 |
+
chart_df["Short"] = chart_df["Model"].astype(str).str.split("huggingface.co/").str[-1]
|
| 894 |
+
chart_df["KL Divergence"] = pd.to_numeric(chart_df["KL Divergence"], errors="coerce")
|
| 895 |
+
chart_df["Refusal Rate (%)"] = pd.to_numeric(chart_df["Refusal Rate (%)"], errors="coerce")
|
| 896 |
+
chart_df["Likes"] = pd.to_numeric(chart_df["Likes"], errors="coerce").fillna(0)
|
| 897 |
+
chart_df["Downloads"] = pd.to_numeric(chart_df["Downloads"], errors="coerce").fillna(0)
|
| 898 |
+
scatter_df = chart_df.dropna(subset=["KL Divergence", "Refusal Rate (%)"])
|
| 899 |
+
if scatter_df.empty:
|
| 900 |
+
st.info("No valid KL / Refusal Rate values to plot.")
|
| 901 |
+
else:
|
| 902 |
+
st.subheader("KL (x) vs Refusal Rate (y), size=Downloads")
|
| 903 |
+
scatter = (
|
| 904 |
+
alt.Chart(scatter_df)
|
| 905 |
+
.mark_circle(opacity=0.7)
|
| 906 |
+
.encode(
|
| 907 |
+
x=alt.X("KL Divergence:Q", scale=alt.Scale(zero=False)),
|
| 908 |
+
y=alt.Y("Refusal Rate (%):Q", scale=alt.Scale(zero=False)),
|
| 909 |
+
size=alt.Size("Downloads:Q", scale=alt.Scale(range=[20, 500])),
|
| 910 |
+
tooltip=["Short:N", "KL Divergence:Q", "Refusal Rate (%):Q", "Likes:Q", "Downloads:Q"],
|
| 911 |
+
)
|
| 912 |
+
.interactive()
|
| 913 |
+
)
|
| 914 |
+
st.altair_chart(scatter, use_container_width=True)
|
| 915 |
+
st.subheader("Refusal Rate per model (top 20 by Likes)")
|
| 916 |
+
top20 = chart_df.nlargest(20, "Likes").sort_values("Refusal Rate (%)", ascending=True)
|
| 917 |
+
top20 = top20.dropna(subset=["Refusal Rate (%)"])
|
| 918 |
+
if top20.empty:
|
| 919 |
+
st.info("No valid Refusal Rate values for bar chart.")
|
| 920 |
+
else:
|
| 921 |
+
bars = (
|
| 922 |
+
alt.Chart(top20)
|
| 923 |
+
.mark_bar()
|
| 924 |
+
.encode(
|
| 925 |
+
x=alt.X("Refusal Rate (%):Q"),
|
| 926 |
+
y=alt.Y("Short:N", sort="-x", title="Model"),
|
| 927 |
+
tooltip=["Short:N", "Refusal Rate (%):Q", "KL Divergence:Q", "Likes:Q", "Downloads:Q"],
|
| 928 |
+
)
|
| 929 |
+
)
|
| 930 |
+
st.altair_chart(bars, use_container_width=True)
|
| 931 |
+
st.subheader("Distribution of KL Divergence")
|
| 932 |
+
kl_df = chart_df.dropna(subset=["KL Divergence"])
|
| 933 |
+
if kl_df.empty:
|
| 934 |
+
st.info("No valid KL values for histogram.")
|
| 935 |
+
else:
|
| 936 |
+
hist = (
|
| 937 |
+
alt.Chart(kl_df)
|
| 938 |
+
.mark_bar()
|
| 939 |
+
.encode(
|
| 940 |
+
x=alt.X("KL Divergence:Q", bin=alt.Bin(maxbins=20)),
|
| 941 |
+
y=alt.Y("count():Q", title="Models"),
|
| 942 |
+
tooltip=[alt.Tooltip("count():Q", title="Models")],
|
| 943 |
+
)
|
| 944 |
+
)
|
| 945 |
+
st.altair_chart(hist, use_container_width=True)
|
data/.gitkeep
ADDED
|
File without changes
|
data/popularity.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"models": {}, "updated_at": null}
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
explorer:
|
| 3 |
+
build: .
|
| 4 |
+
image: heretic-models-explorer:pr1
|
| 5 |
+
ports:
|
| 6 |
+
- "8501:8501"
|
| 7 |
+
environment:
|
| 8 |
+
- STREAMLIT_SERVER_HEADLESS=true
|
| 9 |
+
restart: unless-stopped
|
src/popularity_store.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Popularity store: likes/downloads separated from reasoning metrics.
|
| 2 |
+
|
| 3 |
+
Why this file exists:
|
| 4 |
+
- Likes/Downloads are *volatile* HF API metadata (change hourly).
|
| 5 |
+
- KL Divergence / Refusals are *expensive* parsed reasoning metrics
|
| 6 |
+
(README + reproduce.json fetch + regex, 20 workers).
|
| 7 |
+
- Mixing them in one cache key (`cached_get_model_data(model_id, likes, downloads)`)
|
| 8 |
+
churns the expensive cache every time someone likes a model.
|
| 9 |
+
|
| 10 |
+
Well-grounded approach: yes β keep them separate.
|
| 11 |
+
- Reasoning cache: keyed by model_id only, long TTL (1h+).
|
| 12 |
+
- Popularity: short TTL snapshot + refreshable JSON file
|
| 13 |
+
(`data/popularity.json`) that can be rewritten with minimal impact
|
| 14 |
+
(no re-parse of READMEs, just a cheap HfApi list).
|
| 15 |
+
|
| 16 |
+
File format (data/popularity.json):
|
| 17 |
+
{
|
| 18 |
+
"updated_at": "2026-09-14T12:00:00+00:00",
|
| 19 |
+
"models": {
|
| 20 |
+
"<model_id>": {"likes": 12, "downloads": 345, "url": "https://..."},
|
| 21 |
+
...
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
Missing file -> treated as empty map (graceful, Docker-safe).
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
import json
|
| 28 |
+
import os
|
| 29 |
+
from datetime import datetime, timezone
|
| 30 |
+
|
| 31 |
+
# Resolve repo-root/data/popularity.json from either:
|
| 32 |
+
# /app/src/popularity_store.py -> /app/data/popularity.json (Docker)
|
| 33 |
+
# <root>/src/popularity_store.py -> <root>/data/popularity.json (local)
|
| 34 |
+
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 35 |
+
_CANDIDATES = [
|
| 36 |
+
os.path.join(os.path.dirname(_THIS_DIR), "data", "popularity.json"),
|
| 37 |
+
os.path.join(os.getcwd(), "data", "popularity.json"),
|
| 38 |
+
os.path.join("/app", "data", "popularity.json"),
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def popularity_path() -> str:
|
| 43 |
+
for p in _CANDIDATES:
|
| 44 |
+
d = os.path.dirname(p)
|
| 45 |
+
if os.path.isdir(d):
|
| 46 |
+
return p
|
| 47 |
+
return _CANDIDATES[0]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def load_popularity() -> dict:
|
| 51 |
+
"""Return {model_id: {likes, downloads, url}}. Empty dict if missing/corrupt."""
|
| 52 |
+
path = popularity_path()
|
| 53 |
+
try:
|
| 54 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 55 |
+
data = json.load(f)
|
| 56 |
+
models = data.get("models", {}) if isinstance(data, dict) else {}
|
| 57 |
+
return models if isinstance(models, dict) else {}
|
| 58 |
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
| 59 |
+
return {}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def save_popularity(models_map: dict) -> str:
|
| 63 |
+
"""Persist {model_id: {likes, downloads}} snapshot. Returns path written."""
|
| 64 |
+
path = popularity_path()
|
| 65 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 66 |
+
payload = {
|
| 67 |
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
| 68 |
+
"models": models_map,
|
| 69 |
+
}
|
| 70 |
+
tmp = path + ".tmp"
|
| 71 |
+
with open(tmp, "w", encoding="utf-8") as f:
|
| 72 |
+
json.dump(payload, f, indent=2, sort_keys=True)
|
| 73 |
+
os.replace(tmp, path)
|
| 74 |
+
return path
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def snapshot_from_metadata(metadata: list) -> dict:
|
| 78 |
+
"""Build file-ready map from cheap HfApi metadata list."""
|
| 79 |
+
out = {}
|
| 80 |
+
for d in metadata or []:
|
| 81 |
+
mid = d.get("Model ID")
|
| 82 |
+
if not mid:
|
| 83 |
+
continue
|
| 84 |
+
try:
|
| 85 |
+
likes = int(d.get("Likes", 0) or 0)
|
| 86 |
+
except (TypeError, ValueError):
|
| 87 |
+
likes = 0
|
| 88 |
+
try:
|
| 89 |
+
downloads = int(d.get("Downloads", 0) or 0)
|
| 90 |
+
except (TypeError, ValueError):
|
| 91 |
+
downloads = 0
|
| 92 |
+
out[mid] = {"likes": likes, "downloads": downloads, "url": d.get("URL")}
|
| 93 |
+
return out
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def merge_popularity(rows: list, metadata: list, file_map: dict | None = None) -> list:
|
| 97 |
+
"""Enrich parsed reasoning rows with likes/downloads.
|
| 98 |
+
|
| 99 |
+
Precedence per model: file snapshot > live metadata > row's own values.
|
| 100 |
+
This lets you refresh popularity (rewrite one JSON) without touching
|
| 101 |
+
the expensive KL/refusal cache.
|
| 102 |
+
"""
|
| 103 |
+
if file_map is None:
|
| 104 |
+
file_map = load_popularity()
|
| 105 |
+
meta_map = {d.get("Model ID"): d for d in (metadata or [])}
|
| 106 |
+
for r in rows or []:
|
| 107 |
+
mid = r.get("Model ID")
|
| 108 |
+
file_hit = (file_map or {}).get(mid, {}) if mid else {}
|
| 109 |
+
meta_hit = meta_map.get(mid, {}) if mid else {}
|
| 110 |
+
if isinstance(file_hit, dict) and ("likes" in file_hit or "downloads" in file_hit):
|
| 111 |
+
try:
|
| 112 |
+
r["Likes"] = int(file_hit.get("likes", r.get("Likes", 0)) or 0)
|
| 113 |
+
except (TypeError, ValueError):
|
| 114 |
+
pass
|
| 115 |
+
try:
|
| 116 |
+
r["Downloads"] = int(file_hit.get("downloads", r.get("Downloads", 0)) or 0)
|
| 117 |
+
except (TypeError, ValueError):
|
| 118 |
+
pass
|
| 119 |
+
else:
|
| 120 |
+
if mid in meta_map:
|
| 121 |
+
try:
|
| 122 |
+
r["Likes"] = int(meta_hit.get("Likes", r.get("Likes", 0)) or 0)
|
| 123 |
+
except (TypeError, ValueError):
|
| 124 |
+
pass
|
| 125 |
+
try:
|
| 126 |
+
r["Downloads"] = int(meta_hit.get("Downloads", r.get("Downloads", 0)) or 0)
|
| 127 |
+
except (TypeError, ValueError):
|
| 128 |
+
pass
|
| 129 |
+
return rows
|
src/streamlit_app.py
CHANGED
|
@@ -2,9 +2,12 @@ import streamlit as st
|
|
| 2 |
import pandas as pd
|
| 3 |
from huggingface_hub import HfApi
|
| 4 |
import requests
|
|
|
|
| 5 |
import re
|
| 6 |
import concurrent.futures
|
| 7 |
import logging
|
|
|
|
|
|
|
| 8 |
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
|
| 9 |
|
| 10 |
logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
@@ -20,13 +23,73 @@ st.markdown(
|
|
| 20 |
"allowing you to sort and compare them easily. Click on any column header to sort!"
|
| 21 |
)
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
@retry(
|
| 24 |
retry=retry_if_exception_type((requests.exceptions.ConnectionError, requests.exceptions.Timeout, requests.exceptions.HTTPError)),
|
| 25 |
stop=stop_after_attempt(3),
|
| 26 |
wait=wait_exponential(multiplier=1, min=2, max=10)
|
| 27 |
)
|
| 28 |
def fetch_url(url):
|
| 29 |
-
response =
|
| 30 |
# Raise HTTPError for rate limits (429) or server errors (5xx) to trigger a retry
|
| 31 |
if response.status_code in (429, 500, 502, 503, 504):
|
| 32 |
response.raise_for_status()
|
|
@@ -40,14 +103,14 @@ def fetch_model_info(model):
|
|
| 40 |
refusals_str = None
|
| 41 |
refusal_rate = None
|
| 42 |
initial_refusals_str = None
|
| 43 |
-
|
| 44 |
# Download the README.md via raw URL for high-speed fetching
|
| 45 |
url = f"https://huggingface.co/{model_id}/raw/main/README.md"
|
| 46 |
try:
|
| 47 |
response = fetch_url(url)
|
| 48 |
if response.status_code == 200:
|
| 49 |
readme_text = response.text
|
| 50 |
-
|
| 51 |
# Extract KL divergence using Regex
|
| 52 |
# Matches formats like: "KL divergence | 0.0033"
|
| 53 |
kl_match = re.search(r"(?i)KL\s*divergence[^a-zA-Z\d\n]*?([\d\.]+)", readme_text)
|
|
@@ -56,7 +119,7 @@ def fetch_model_info(model):
|
|
| 56 |
kl_div = float(kl_match.group(1))
|
| 57 |
except ValueError as e:
|
| 58 |
logging.warning(f"Could not parse KL divergence for {model_id}: {e}")
|
| 59 |
-
|
| 60 |
# Extract Refusals using Regex
|
| 61 |
# Matches formats like: "Refusals | 15/100" or "15 / 100"
|
| 62 |
for line in readme_text.split('\n'):
|
|
@@ -65,19 +128,19 @@ def fetch_model_info(model):
|
|
| 65 |
fractions = re.findall(r"(\d+)\s*/\s*(\d+)", line)
|
| 66 |
if not fractions:
|
| 67 |
continue
|
| 68 |
-
|
| 69 |
if 'initial' in line_lower and initial_refusals_str is None:
|
| 70 |
initial_refusals_str = f"{fractions[0][0]}/{fractions[0][1]}"
|
| 71 |
elif 'initial' not in line_lower and refusals_str is None:
|
| 72 |
refusals_str = f"{fractions[0][0]}/{fractions[0][1]}"
|
| 73 |
try:
|
| 74 |
-
refusal_rate = (int(fractions[0][0]) / int(fractions[0][1])) * 100
|
| 75 |
except (ValueError, ZeroDivisionError) as e:
|
| 76 |
logging.warning(f"Could not parse refusals for {model_id}: {e}")
|
| 77 |
continue
|
| 78 |
if len(fractions) >= 2 and initial_refusals_str is None:
|
| 79 |
initial_refusals_str = f"{fractions[1][0]}/{fractions[1][1]}"
|
| 80 |
-
|
| 81 |
except requests.exceptions.RequestException as e:
|
| 82 |
logging.warning(f"Error fetching README for {model_id}: {e}")
|
| 83 |
except Exception as e:
|
|
@@ -98,7 +161,7 @@ def fetch_model_info(model):
|
|
| 98 |
"URL": f"https://huggingface.co/{model_id}"
|
| 99 |
}
|
| 100 |
|
| 101 |
-
# Check for reproducibility record first and then fallback to parsing data from readme.md
|
| 102 |
def get_model_data(model):
|
| 103 |
model_id = model.id
|
| 104 |
url = f"https://huggingface.co/{model_id}/raw/main/reproduce/reproduce.json"
|
|
@@ -134,58 +197,749 @@ def get_model_data(model):
|
|
| 134 |
logging.error(f"Unexpected error processing JSON for {model_id}: {e}")
|
| 135 |
return fetch_model_info(model)
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
api = HfApi()
|
| 141 |
-
|
| 142 |
# Query all models using the Hugging Face Hub `filter` parameter
|
| 143 |
models = list(api.list_models(filter="heretic"))
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
data = []
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
| 154 |
for res in results:
|
| 155 |
if res is not None:
|
| 156 |
data.append(res)
|
| 157 |
-
|
| 158 |
return data
|
| 159 |
|
| 160 |
-
#
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
else:
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import pandas as pd
|
| 3 |
from huggingface_hub import HfApi
|
| 4 |
import requests
|
| 5 |
+
from requests.adapters import HTTPAdapter
|
| 6 |
import re
|
| 7 |
import concurrent.futures
|
| 8 |
import logging
|
| 9 |
+
from datetime import datetime, timezone
|
| 10 |
+
from types import SimpleNamespace
|
| 11 |
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
|
| 12 |
|
| 13 |
logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
|
|
| 23 |
"allowing you to sort and compare them easily. Click on any column header to sort!"
|
| 24 |
)
|
| 25 |
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
# Filter config (single source of truth)
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
FILTER_DEFAULTS = {
|
| 30 |
+
"query": "",
|
| 31 |
+
"date_range": "All time",
|
| 32 |
+
"unknowns": "Include unknowns",
|
| 33 |
+
"kl_range": (0.0, 0.5),
|
| 34 |
+
"refusal_range": (0.0, 100.0),
|
| 35 |
+
"min_likes": 0,
|
| 36 |
+
"min_downloads": 0,
|
| 37 |
+
"show_quants": False,
|
| 38 |
+
"sort_by": "KL Divergence (low β high)",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
DATE_OPTIONS = {
|
| 42 |
+
"All time": None,
|
| 43 |
+
"Last week": 7,
|
| 44 |
+
"Last month": 30,
|
| 45 |
+
"Last 6 months": 182,
|
| 46 |
+
"Last year": 365,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
UNKNOWN_OPTIONS = (
|
| 50 |
+
"Include unknowns",
|
| 51 |
+
"Hide unknowns",
|
| 52 |
+
"Only unknowns",
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
SORT_OPTIONS = {
|
| 56 |
+
"KL Divergence (low β high)": ("KL Divergence", True),
|
| 57 |
+
"KL Divergence (high β low)": ("KL Divergence", False),
|
| 58 |
+
"Refusal Rate (%) (low β high)": ("Refusal Rate (%)", True),
|
| 59 |
+
"Refusal Rate (%) (high β low)": ("Refusal Rate (%)", False),
|
| 60 |
+
"Likes (high β low)": ("Likes", False),
|
| 61 |
+
"Downloads (high β low)": ("Downloads", False),
|
| 62 |
+
"Model (A β Z)": ("Model", True),
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
QUANT_PATTERNS = re.compile(r'(?i)(gguf|mlx|awq|nvfp4|gptq|exl[23]|-quant|int8|int4|oq[1-8]|mxfp[48])')
|
| 66 |
+
QUANT_RE = r"(?i)(gguf|mlx|awq|nvfp4|gptq|exl[23]|-quant|int8|int4|oq[1-8]|mxfp[48])"
|
| 67 |
+
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
+
# Network: reused Session + retry
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
_SESSION = None
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _get_session():
|
| 75 |
+
global _SESSION
|
| 76 |
+
if _SESSION is None:
|
| 77 |
+
s = requests.Session()
|
| 78 |
+
adapter = HTTPAdapter(pool_connections=20, pool_maxsize=20, max_retries=0)
|
| 79 |
+
s.mount("https://", adapter)
|
| 80 |
+
s.mount("http://", adapter)
|
| 81 |
+
s.headers.update({"User-Agent": "Heretic-Models-Explorer/1.0"})
|
| 82 |
+
_SESSION = s
|
| 83 |
+
return _SESSION
|
| 84 |
+
|
| 85 |
+
|
| 86 |
@retry(
|
| 87 |
retry=retry_if_exception_type((requests.exceptions.ConnectionError, requests.exceptions.Timeout, requests.exceptions.HTTPError)),
|
| 88 |
stop=stop_after_attempt(3),
|
| 89 |
wait=wait_exponential(multiplier=1, min=2, max=10)
|
| 90 |
)
|
| 91 |
def fetch_url(url):
|
| 92 |
+
response = _get_session().get(url, timeout=(3.05, 5))
|
| 93 |
# Raise HTTPError for rate limits (429) or server errors (5xx) to trigger a retry
|
| 94 |
if response.status_code in (429, 500, 502, 503, 504):
|
| 95 |
response.raise_for_status()
|
|
|
|
| 103 |
refusals_str = None
|
| 104 |
refusal_rate = None
|
| 105 |
initial_refusals_str = None
|
| 106 |
+
|
| 107 |
# Download the README.md via raw URL for high-speed fetching
|
| 108 |
url = f"https://huggingface.co/{model_id}/raw/main/README.md"
|
| 109 |
try:
|
| 110 |
response = fetch_url(url)
|
| 111 |
if response.status_code == 200:
|
| 112 |
readme_text = response.text
|
| 113 |
+
|
| 114 |
# Extract KL divergence using Regex
|
| 115 |
# Matches formats like: "KL divergence | 0.0033"
|
| 116 |
kl_match = re.search(r"(?i)KL\s*divergence[^a-zA-Z\d\n]*?([\d\.]+)", readme_text)
|
|
|
|
| 119 |
kl_div = float(kl_match.group(1))
|
| 120 |
except ValueError as e:
|
| 121 |
logging.warning(f"Could not parse KL divergence for {model_id}: {e}")
|
| 122 |
+
|
| 123 |
# Extract Refusals using Regex
|
| 124 |
# Matches formats like: "Refusals | 15/100" or "15 / 100"
|
| 125 |
for line in readme_text.split('\n'):
|
|
|
|
| 128 |
fractions = re.findall(r"(\d+)\s*/\s*(\d+)", line)
|
| 129 |
if not fractions:
|
| 130 |
continue
|
| 131 |
+
|
| 132 |
if 'initial' in line_lower and initial_refusals_str is None:
|
| 133 |
initial_refusals_str = f"{fractions[0][0]}/{fractions[0][1]}"
|
| 134 |
elif 'initial' not in line_lower and refusals_str is None:
|
| 135 |
refusals_str = f"{fractions[0][0]}/{fractions[0][1]}"
|
| 136 |
try:
|
| 137 |
+
refusal_rate = (int(fractions[0][0]) / int(fractions[0][1])) * 100
|
| 138 |
except (ValueError, ZeroDivisionError) as e:
|
| 139 |
logging.warning(f"Could not parse refusals for {model_id}: {e}")
|
| 140 |
continue
|
| 141 |
if len(fractions) >= 2 and initial_refusals_str is None:
|
| 142 |
initial_refusals_str = f"{fractions[1][0]}/{fractions[1][1]}"
|
| 143 |
+
|
| 144 |
except requests.exceptions.RequestException as e:
|
| 145 |
logging.warning(f"Error fetching README for {model_id}: {e}")
|
| 146 |
except Exception as e:
|
|
|
|
| 161 |
"URL": f"https://huggingface.co/{model_id}"
|
| 162 |
}
|
| 163 |
|
| 164 |
+
# Check for reproducibility record first and then fallback to parsing data from readme.md
|
| 165 |
def get_model_data(model):
|
| 166 |
model_id = model.id
|
| 167 |
url = f"https://huggingface.co/{model_id}/raw/main/reproduce/reproduce.json"
|
|
|
|
| 197 |
logging.error(f"Unexpected error processing JSON for {model_id}: {e}")
|
| 198 |
return fetch_model_info(model)
|
| 199 |
|
| 200 |
+
# ---------------------------------------------------------------------------
|
| 201 |
+
# Two-stage cached loading (faster initial load)
|
| 202 |
+
# ---------------------------------------------------------------------------
|
| 203 |
+
@st.cache_data(ttl=3600, show_spinner=False)
|
| 204 |
+
def list_heretic_metadata():
|
| 205 |
+
"""Stage 1: HfApi only, no per-model fetch. Cheap and fast.
|
| 206 |
+
|
| 207 |
+
Returns ALL models (including quants) with Created/LastModified dates
|
| 208 |
+
so the caller can pre-filter by date/query BEFORE the expensive
|
| 209 |
+
per-model README/reproduce.json parsing.
|
| 210 |
+
"""
|
| 211 |
api = HfApi()
|
|
|
|
| 212 |
# Query all models using the Hugging Face Hub `filter` parameter
|
| 213 |
models = list(api.list_models(filter="heretic"))
|
| 214 |
+
out = []
|
| 215 |
+
for m in models:
|
| 216 |
+
mid = m.id
|
| 217 |
+
created = getattr(m, "created_at", None)
|
| 218 |
+
modified = getattr(m, "last_modified", None)
|
| 219 |
+
# Normalise to ISO strings (cache-safe, JSON-safe)
|
| 220 |
+
try:
|
| 221 |
+
created_iso = created.isoformat() if hasattr(created, "isoformat") else (str(created) if created else None)
|
| 222 |
+
except Exception:
|
| 223 |
+
created_iso = None
|
| 224 |
+
try:
|
| 225 |
+
modified_iso = modified.isoformat() if hasattr(modified, "isoformat") else (str(modified) if modified else None)
|
| 226 |
+
except Exception:
|
| 227 |
+
modified_iso = None
|
| 228 |
+
out.append({
|
| 229 |
+
"Model ID": mid,
|
| 230 |
+
"Likes": getattr(m, "likes", 0) or 0,
|
| 231 |
+
"Downloads": getattr(m, "downloads", 0) or 0,
|
| 232 |
+
"URL": f"https://huggingface.co/{mid}",
|
| 233 |
+
"Created": created_iso,
|
| 234 |
+
"LastModified": modified_iso,
|
| 235 |
+
})
|
| 236 |
+
return out
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@st.cache_data(ttl=3600, show_spinner=False)
|
| 240 |
+
def cached_get_model_data(model_id: str, likes: int = 0, downloads: int = 0):
|
| 241 |
+
"""Stage 2: per-model reasoning cache, keyed by model_id only.
|
| 242 |
+
|
| 243 |
+
likes/downloads are accepted for back-compat but IGNORED in the key path:
|
| 244 |
+
popularity is merged later via popularity_store so a like-spike doesn't
|
| 245 |
+
invalidate the expensive KL/refusal parse. Pass model_id alone going forward.
|
| 246 |
+
"""
|
| 247 |
+
proxy = SimpleNamespace(id=model_id, likes=0, downloads=0)
|
| 248 |
+
return get_model_data(proxy)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
@st.cache_data(ttl=3600, show_spinner=False) # Cache for 1 hour, back-compat shim
|
| 252 |
+
def get_heretic_models():
|
| 253 |
+
"""Fetches all heretic models and their metrics concurrently."""
|
| 254 |
+
meta = list_heretic_metadata()
|
| 255 |
data = []
|
| 256 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
|
| 257 |
+
results = executor.map(
|
| 258 |
+
lambda d: cached_get_model_data(d["Model ID"], d["Likes"], d["Downloads"]),
|
| 259 |
+
meta,
|
| 260 |
+
)
|
| 261 |
for res in results:
|
| 262 |
if res is not None:
|
| 263 |
data.append(res)
|
|
|
|
| 264 |
return data
|
| 265 |
|
| 266 |
+
# ---------------------------------------------------------------------------
|
| 267 |
+
# Filtering (pure, testable)
|
| 268 |
+
# ---------------------------------------------------------------------------
|
| 269 |
+
def _model_id_series(df: pd.DataFrame) -> pd.Series:
|
| 270 |
+
col = "Model ID" if "Model ID" in df.columns else "Model"
|
| 271 |
+
if col in df.columns:
|
| 272 |
+
return df[col].astype("string")
|
| 273 |
+
return pd.Series([""] * len(df), index=df.index, dtype="string")
|
| 274 |
|
| 275 |
+
|
| 276 |
+
def _parse_created(v):
|
| 277 |
+
"""Parse Created ISO string to aware datetime, or None."""
|
| 278 |
+
if v is None or (isinstance(v, float) and pd.isna(v)):
|
| 279 |
+
return None
|
| 280 |
+
if isinstance(v, datetime):
|
| 281 |
+
dt = v
|
| 282 |
+
else:
|
| 283 |
+
try:
|
| 284 |
+
dt = datetime.fromisoformat(str(v).replace("Z", "+00:00"))
|
| 285 |
+
except Exception:
|
| 286 |
+
return None
|
| 287 |
+
if dt is not None and dt.tzinfo is None:
|
| 288 |
+
dt = dt.replace(tzinfo=timezone.utc)
|
| 289 |
+
return dt
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def filter_metadata_by_date(metadata: list, date_range_key: str, unknowns_key: str = "Include unknowns") -> list:
|
| 293 |
+
"""Pre-filter raw metadata list by Created date BEFORE expensive parsing.
|
| 294 |
+
|
| 295 |
+
Unknowns handling is now an explicit user choice (not silently kept):
|
| 296 |
+
- "Include unknowns": keep models with missing Created date (old default).
|
| 297 |
+
- "Hide unknowns": drop them β strictest, smallest parse set.
|
| 298 |
+
- "Only unknowns": parse ONLY models with missing dates (audit mode).
|
| 299 |
+
"""
|
| 300 |
+
days = DATE_OPTIONS.get(date_range_key, None)
|
| 301 |
+
if unknowns_key not in UNKNOWN_OPTIONS:
|
| 302 |
+
unknowns_key = "Include unknowns"
|
| 303 |
+
if days is None and unknowns_key == "Include unknowns":
|
| 304 |
+
return list(metadata)
|
| 305 |
+
cutoff = None
|
| 306 |
+
if days is not None:
|
| 307 |
+
cutoff = datetime.now(timezone.utc) - pd.Timedelta(days=days).to_pytimedelta()
|
| 308 |
+
kept = []
|
| 309 |
+
for d in metadata:
|
| 310 |
+
dt = _parse_created(d.get("Created"))
|
| 311 |
+
if dt is None:
|
| 312 |
+
if unknowns_key in ("Include unknowns", "Only unknowns"):
|
| 313 |
+
kept.append(d)
|
| 314 |
+
elif cutoff is None:
|
| 315 |
+
# All time + Hide unknowns -> drop unknowns (already skipped), keep dated
|
| 316 |
+
kept.append(d)
|
| 317 |
+
elif unknowns_key == "Only unknowns":
|
| 318 |
+
continue # dated rows excluded in audit mode
|
| 319 |
+
elif dt >= cutoff:
|
| 320 |
+
kept.append(d)
|
| 321 |
+
return kept
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def prefilter_metadata(metadata: list, query: str, show_quants: bool, date_range_key: str, unknowns_key: str = "Include unknowns") -> list:
|
| 325 |
+
"""Cheap pre-fetch filter: date (+unknowns handling) + quant + query. No network."""
|
| 326 |
+
out = filter_metadata_by_date(metadata, date_range_key, unknowns_key)
|
| 327 |
+
if not show_quants:
|
| 328 |
+
out = [d for d in out if not QUANT_PATTERNS.search(d.get("Model ID", ""))]
|
| 329 |
+
if query is not None:
|
| 330 |
+
q = str(query).strip().lower()
|
| 331 |
+
if q:
|
| 332 |
+
out = [d for d in out if q in str(d.get("Model ID", "")).lower()]
|
| 333 |
+
return out
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def apply_filters(df, query, kl_range, refusal_range,
|
| 337 |
+
min_likes, min_downloads, show_quants, date_range="All time", unknowns="Include unknowns"):
|
| 338 |
+
"""Pure filter. Never mutates input. Handles empty df, NaNs, case-insensitive query."""
|
| 339 |
+
if df is None or len(df) == 0:
|
| 340 |
+
return df.copy() if isinstance(df, pd.DataFrame) else pd.DataFrame()
|
| 341 |
+
mask = pd.Series(True, index=df.index)
|
| 342 |
+
if query is not None:
|
| 343 |
+
q = str(query).strip()
|
| 344 |
+
if q:
|
| 345 |
+
ids = _model_id_series(df).fillna("")
|
| 346 |
+
mask &= ids.str.contains(q, case=False, na=False, regex=False)
|
| 347 |
+
if kl_range is not None and "KL Divergence" in df.columns:
|
| 348 |
+
try:
|
| 349 |
+
lo, hi = float(kl_range[0]), float(kl_range[1])
|
| 350 |
+
lo, hi = (hi, lo) if lo > hi else (lo, hi)
|
| 351 |
+
kl = pd.to_numeric(df["KL Divergence"], errors="coerce")
|
| 352 |
+
mask &= kl.between(lo, hi, inclusive="both")
|
| 353 |
+
except (TypeError, ValueError, IndexError):
|
| 354 |
+
pass
|
| 355 |
+
if refusal_range is not None and "Refusal Rate (%)" in df.columns:
|
| 356 |
+
try:
|
| 357 |
+
lo, hi = float(refusal_range[0]), float(refusal_range[1])
|
| 358 |
+
lo, hi = (hi, lo) if lo > hi else (lo, hi)
|
| 359 |
+
rr = pd.to_numeric(df["Refusal Rate (%)"], errors="coerce")
|
| 360 |
+
mask &= rr.between(lo, hi, inclusive="both")
|
| 361 |
+
except (TypeError, ValueError, IndexError):
|
| 362 |
+
pass
|
| 363 |
+
for col, thresh in (("Likes", min_likes), ("Downloads", min_downloads)):
|
| 364 |
+
if col in df.columns and thresh is not None:
|
| 365 |
+
try:
|
| 366 |
+
t = float(thresh)
|
| 367 |
+
except (TypeError, ValueError):
|
| 368 |
+
continue
|
| 369 |
+
if pd.isna(t) or t <= 0:
|
| 370 |
+
continue
|
| 371 |
+
vals = pd.to_numeric(df[col], errors="coerce").fillna(0)
|
| 372 |
+
mask &= vals >= t
|
| 373 |
+
if not show_quants:
|
| 374 |
+
ids = _model_id_series(df).fillna("")
|
| 375 |
+
mask &= ~ids.str.contains(QUANT_RE, case=False, na=False, regex=True)
|
| 376 |
+
# Date filter on Created column (post-fetch safety net; main saving is pre-fetch)
|
| 377 |
+
days = DATE_OPTIONS.get(date_range, None)
|
| 378 |
+
if unknowns not in UNKNOWN_OPTIONS:
|
| 379 |
+
unknowns = "Include unknowns"
|
| 380 |
+
if "Created" in df.columns and (days is not None or unknowns != "Include unknowns"):
|
| 381 |
+
try:
|
| 382 |
+
cutoff = None
|
| 383 |
+
if days is not None:
|
| 384 |
+
cutoff = datetime.now(timezone.utc) - pd.Timedelta(days=days).to_pytimedelta()
|
| 385 |
+
parsed = df["Created"].apply(_parse_created)
|
| 386 |
+
if unknowns == "Only unknowns":
|
| 387 |
+
date_mask = parsed.apply(lambda dt: dt is None)
|
| 388 |
+
elif unknowns == "Hide unknowns":
|
| 389 |
+
if cutoff is None:
|
| 390 |
+
date_mask = parsed.apply(lambda dt: dt is not None)
|
| 391 |
+
else:
|
| 392 |
+
date_mask = parsed.apply(lambda dt: dt is not None and dt >= cutoff)
|
| 393 |
+
else: # Include unknowns
|
| 394 |
+
if cutoff is None:
|
| 395 |
+
date_mask = pd.Series(True, index=df.index)
|
| 396 |
+
else:
|
| 397 |
+
date_mask = parsed.apply(lambda dt: True if dt is None else dt >= cutoff)
|
| 398 |
+
mask &= date_mask.fillna(True).to_numpy() if hasattr(date_mask, "fillna") else date_mask
|
| 399 |
+
except Exception:
|
| 400 |
+
pass
|
| 401 |
+
return df.loc[mask].copy().reset_index(drop=True)
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def _qp_first(key, default=""):
|
| 405 |
+
v = st.query_params.get(key, default)
|
| 406 |
+
return v[0] if isinstance(v, list) and v else v
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def _to_float(v, default):
|
| 410 |
+
try:
|
| 411 |
+
f = float(v)
|
| 412 |
+
return f if not pd.isna(f) else default
|
| 413 |
+
except (TypeError, ValueError):
|
| 414 |
+
return default
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def _to_int(v, default):
|
| 418 |
+
try:
|
| 419 |
+
return max(0, int(float(v)))
|
| 420 |
+
except (TypeError, ValueError):
|
| 421 |
+
return default
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def init_filter_state_from_query_params():
|
| 425 |
+
if st.session_state.get("_qp_init"):
|
| 426 |
+
return
|
| 427 |
+
d = FILTER_DEFAULTS
|
| 428 |
+
kl_lo = _to_float(_qp_first("kl_min", d["kl_range"][0]), d["kl_range"][0])
|
| 429 |
+
kl_hi = _to_float(_qp_first("kl_max", d["kl_range"][1]), d["kl_range"][1])
|
| 430 |
+
rr_lo = _to_float(_qp_first("rr_min", d["refusal_range"][0]), d["refusal_range"][0])
|
| 431 |
+
rr_hi = _to_float(_qp_first("rr_max", d["refusal_range"][1]), d["refusal_range"][1])
|
| 432 |
+
st.session_state.setdefault("query", str(_qp_first("q", d["query"])))
|
| 433 |
+
_dr = str(_qp_first("date_range", d["date_range"]))
|
| 434 |
+
st.session_state.setdefault("date_range", _dr if _dr in DATE_OPTIONS else d["date_range"])
|
| 435 |
+
_unk = str(_qp_first("unknowns", d["unknowns"]))
|
| 436 |
+
st.session_state.setdefault("unknowns", _unk if _unk in UNKNOWN_OPTIONS else d["unknowns"])
|
| 437 |
+
st.session_state.setdefault("kl_range", (min(kl_lo, kl_hi), max(kl_lo, kl_hi)))
|
| 438 |
+
st.session_state.setdefault("refusal_range", (min(rr_lo, rr_hi), max(rr_lo, rr_hi)))
|
| 439 |
+
st.session_state.setdefault("min_likes", _to_int(_qp_first("min_likes", d["min_likes"]), d["min_likes"]))
|
| 440 |
+
st.session_state.setdefault("min_downloads", _to_int(_qp_first("min_downloads", d["min_downloads"]), d["min_downloads"]))
|
| 441 |
+
st.session_state.setdefault("show_quants", str(_qp_first("show_quants", "")).lower() in ("1", "true", "yes"))
|
| 442 |
+
sort = str(_qp_first("sort_by", d["sort_by"]))
|
| 443 |
+
st.session_state.setdefault("sort_by", sort if sort in SORT_OPTIONS else d["sort_by"])
|
| 444 |
+
st.session_state["_qp_init"] = True
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def sync_filter_state_to_query_params():
|
| 448 |
+
st.query_params["q"] = st.session_state.get("query", "")
|
| 449 |
+
st.query_params["date_range"] = str(st.session_state.get("date_range", FILTER_DEFAULTS["date_range"]))
|
| 450 |
+
st.query_params["unknowns"] = str(st.session_state.get("unknowns", FILTER_DEFAULTS["unknowns"]))
|
| 451 |
+
kl = st.session_state.get("kl_range", FILTER_DEFAULTS["kl_range"])
|
| 452 |
+
rr = st.session_state.get("refusal_range", FILTER_DEFAULTS["refusal_range"])
|
| 453 |
+
st.query_params["kl_min"] = str(float(kl[0]))
|
| 454 |
+
st.query_params["kl_max"] = str(float(kl[1]))
|
| 455 |
+
st.query_params["rr_min"] = str(float(rr[0]))
|
| 456 |
+
st.query_params["rr_max"] = str(float(rr[1]))
|
| 457 |
+
st.query_params["min_likes"] = str(int(st.session_state.get("min_likes", 0) or 0))
|
| 458 |
+
st.query_params["min_downloads"] = str(int(st.session_state.get("min_downloads", 0) or 0))
|
| 459 |
+
st.query_params["show_quants"] = "1" if st.session_state.get("show_quants") else "0"
|
| 460 |
+
st.query_params["sort_by"] = str(st.session_state.get("sort_by", FILTER_DEFAULTS["sort_by"]))
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def _short(url: str) -> str:
|
| 464 |
+
s = str(url)
|
| 465 |
+
return s.split("huggingface.co/")[-1] if "huggingface.co/" in s else s
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def _parse_fraction(s) -> tuple:
|
| 469 |
+
"""Parse '15/100' β (15, 100). Returns (None, None) on failure."""
|
| 470 |
+
if s is None or (isinstance(s, float) and pd.isna(s)):
|
| 471 |
+
return (None, None)
|
| 472 |
+
m = re.search(r"(\d+)\s*/\s*(\d+)", str(s))
|
| 473 |
+
if not m:
|
| 474 |
+
return (None, None)
|
| 475 |
+
try:
|
| 476 |
+
return (int(m.group(1)), int(m.group(2)))
|
| 477 |
+
except (ValueError, ZeroDivisionError):
|
| 478 |
+
return (None, None)
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
def _kl_band(kl) -> str:
|
| 482 |
+
"""Rough weight-impact bands. Not a verdict β higher KL = more brain drift."""
|
| 483 |
+
if kl is None or (isinstance(kl, float) and pd.isna(kl)):
|
| 484 |
+
return "unknown"
|
| 485 |
+
try:
|
| 486 |
+
kl = float(kl)
|
| 487 |
+
except (TypeError, ValueError):
|
| 488 |
+
return "unknown"
|
| 489 |
+
if kl < 0.01:
|
| 490 |
+
return "minimal drift"
|
| 491 |
+
if kl < 0.05:
|
| 492 |
+
return "moderate drift"
|
| 493 |
+
if kl < 0.15:
|
| 494 |
+
return "high drift"
|
| 495 |
+
return "very high drift"
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def _compare_note(kl, ref_num, ref_den, init_num, init_den) -> str:
|
| 499 |
+
"""One-line grain-of-salt note per model. No trophies."""
|
| 500 |
+
parts = []
|
| 501 |
+
# Sample-size caution
|
| 502 |
+
if ref_den is None:
|
| 503 |
+
parts.append("unknown sample size β weak evidence")
|
| 504 |
+
elif ref_den < 50:
|
| 505 |
+
parts.append(f"small sample (n={ref_den}) β take with grain of salt")
|
| 506 |
+
# Drop vs base
|
| 507 |
+
if init_num is not None and ref_num is not None and init_den and ref_den:
|
| 508 |
+
drop = init_num - ref_num
|
| 509 |
+
parts.append(f"drop {drop} (base {init_num}/{init_den} β {ref_num}/{ref_den})")
|
| 510 |
+
# KL vs refusal tradeoff β the core complaint: 0% refusal + high KL = stupid
|
| 511 |
+
try:
|
| 512 |
+
klf = float(kl) if kl is not None else None
|
| 513 |
+
except (TypeError, ValueError):
|
| 514 |
+
klf = None
|
| 515 |
+
try:
|
| 516 |
+
rate = (ref_num / ref_den * 100) if ref_num is not None and ref_den else None
|
| 517 |
+
except ZeroDivisionError:
|
| 518 |
+
rate = None
|
| 519 |
+
if rate is not None and klf is not None:
|
| 520 |
+
if rate == 0 and klf > 0.1:
|
| 521 |
+
parts.append("0 refusals BUT high KL β possibly lobotomized, check reasoning")
|
| 522 |
+
elif rate == 0 and klf > 0.05:
|
| 523 |
+
parts.append("0 refusals with notable drift β verify quality")
|
| 524 |
+
elif rate <= 5 and klf is not None and klf < 0.02:
|
| 525 |
+
parts.append("good balance: low refusal + low drift")
|
| 526 |
+
elif rate > 20:
|
| 527 |
+
parts.append("still refusal-prone, regardless of KL")
|
| 528 |
+
elif klf is not None and klf > 0.1:
|
| 529 |
+
parts.append("high KL β expect reasoning damage")
|
| 530 |
+
return "; ".join(parts) if parts else "β"
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
# ---------------------------------------------------------------------------
|
| 534 |
+
# Main execution: metadata instantly, pre-filter by date, then parse subset
|
| 535 |
+
# ---------------------------------------------------------------------------
|
| 536 |
+
init_filter_state_from_query_params()
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def _reset_filters():
|
| 540 |
+
"""Callback for Reset button β runs BEFORE widgets instantiate, so no
|
| 541 |
+
StreamlitWidgetAlreadyInstantiatedError (the crash you saw)."""
|
| 542 |
+
for k, v in FILTER_DEFAULTS.items():
|
| 543 |
+
st.session_state[k] = v
|
| 544 |
+
st.query_params.clear()
|
| 545 |
+
st.session_state.metrics_done = False
|
| 546 |
+
st.session_state.metrics_rows = []
|
| 547 |
+
st.session_state.metrics_signature = None
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
if "metrics_done" not in st.session_state:
|
| 551 |
+
st.session_state.metrics_done = False
|
| 552 |
+
if "metrics_rows" not in st.session_state:
|
| 553 |
+
st.session_state.metrics_rows = []
|
| 554 |
+
if "metrics_signature" not in st.session_state:
|
| 555 |
+
st.session_state.metrics_signature = None
|
| 556 |
+
|
| 557 |
+
# --- EARLY pre-filters: these decide HOW MANY models get parsed ---
|
| 558 |
+
# Shown before any expensive README/reproduce.json fetching.
|
| 559 |
+
st.sidebar.header("Load scope (saves parsing time)")
|
| 560 |
+
early_date = st.sidebar.selectbox(
|
| 561 |
+
"Release date",
|
| 562 |
+
list(DATE_OPTIONS.keys()),
|
| 563 |
+
key="date_range",
|
| 564 |
+
help="Pre-filters by Hugging Face creation date BEFORE parsing. Pick a recent window to avoid parsing ~2000 models.",
|
| 565 |
+
)
|
| 566 |
+
early_unknowns = st.sidebar.selectbox(
|
| 567 |
+
"Unknown dates",
|
| 568 |
+
list(UNKNOWN_OPTIONS),
|
| 569 |
+
key="unknowns",
|
| 570 |
+
help="Models with missing HF creation date. Include (old default), Hide (strictest, smallest parse), or Only (audit them).",
|
| 571 |
+
)
|
| 572 |
+
early_query = st.sidebar.text_input(
|
| 573 |
+
"Search model ID (pre-filter)",
|
| 574 |
+
key="query",
|
| 575 |
+
placeholder="e.g. llama-3 heretic β¦",
|
| 576 |
+
help="Case-insensitive substring. Applied BEFORE parsing to skip non-matching models.",
|
| 577 |
+
)
|
| 578 |
+
early_quants = st.sidebar.checkbox(
|
| 579 |
+
"Show quantised models",
|
| 580 |
+
key="show_quants",
|
| 581 |
+
help="Untick to hide GGUF/MLX/AWQ/GPTQ/EXL2/EXL3/quant/INT8/INT4/OQ/MXFP4/MXFP8 before parsing.",
|
| 582 |
+
)
|
| 583 |
+
try:
|
| 584 |
+
from src.popularity_store import load_popularity, save_popularity, snapshot_from_metadata
|
| 585 |
+
except ImportError:
|
| 586 |
+
try:
|
| 587 |
+
from popularity_store import load_popularity, save_popularity, snapshot_from_metadata
|
| 588 |
+
except ImportError:
|
| 589 |
+
load_popularity = lambda: {} # noqa: E731
|
| 590 |
+
save_popularity = lambda m: "" # noqa: E731
|
| 591 |
+
snapshot_from_metadata = lambda md: {} # noqa: E731
|
| 592 |
+
with st.sidebar.expander("Popularity (likes/downloads)", expanded=False):
|
| 593 |
+
st.caption("Stored separately in data/popularity.json β refreshable without re-parsing READMEs.")
|
| 594 |
+
if st.button("π Refresh popularity file", use_container_width=True):
|
| 595 |
+
try:
|
| 596 |
+
_live = list_heretic_metadata()
|
| 597 |
+
_path = save_popularity(snapshot_from_metadata(_live))
|
| 598 |
+
st.success(f"Saved {len(_live)} entries to {_path or 'data/popularity.json'}")
|
| 599 |
+
except Exception as e:
|
| 600 |
+
st.error(f"Could not save popularity: {e}")
|
| 601 |
+
try:
|
| 602 |
+
_pop = load_popularity()
|
| 603 |
+
st.caption(f"{len(_pop)} models in popularity file.")
|
| 604 |
+
except Exception:
|
| 605 |
+
pass
|
| 606 |
+
|
| 607 |
+
col_a, col_b = st.columns([1, 4])
|
| 608 |
+
with col_a:
|
| 609 |
+
refresh = st.button("π Refresh metrics", use_container_width=True)
|
| 610 |
+
with col_b:
|
| 611 |
+
auto = st.checkbox("Auto-load metrics", value=True)
|
| 612 |
+
|
| 613 |
+
try:
|
| 614 |
+
with st.spinner("Fetching model list (fast metadata)..."):
|
| 615 |
+
metadata = list_heretic_metadata()
|
| 616 |
+
except Exception as e:
|
| 617 |
+
st.error(f"Could not list heretic models: {e}")
|
| 618 |
+
st.stop()
|
| 619 |
+
|
| 620 |
+
# Cheap pre-filter BEFORE expensive parsing β this is the load-time win.
|
| 621 |
+
target_meta = prefilter_metadata(metadata, early_query, early_quants, early_date, early_unknowns)
|
| 622 |
+
st.caption(
|
| 623 |
+
f"Found {len(metadata)} models (metadata cached 1h). "
|
| 624 |
+
f"Scope '{early_date}' + '{early_unknowns}' + search β **{len(target_meta)} to parse**."
|
| 625 |
+
)
|
| 626 |
+
if len(target_meta) == 0:
|
| 627 |
+
st.info("No models in this date/search scope β widen the date range, set Unknowns to Include, or clear search.")
|
| 628 |
+
st.stop()
|
| 629 |
+
if len(target_meta) > 500:
|
| 630 |
+
st.warning(
|
| 631 |
+
f"Scope selects {len(target_meta)} models β parsing will take a while. "
|
| 632 |
+
"Tip: pick 'Last month' / 'Last week', Hide unknowns, or add a search term."
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
fetch_signature = (early_date, early_unknowns, early_query.strip().lower() if early_query else "", bool(early_quants))
|
| 636 |
+
if st.session_state.metrics_signature is not None and st.session_state.metrics_signature != fetch_signature:
|
| 637 |
+
# Scope changed β old parsed rows no longer valid
|
| 638 |
+
st.session_state.metrics_done = False
|
| 639 |
+
st.session_state.metrics_rows = []
|
| 640 |
+
|
| 641 |
+
should_run = (auto and not st.session_state.metrics_done) or refresh
|
| 642 |
+
if refresh:
|
| 643 |
+
st.session_state.metrics_done = False
|
| 644 |
+
st.session_state.metrics_rows = []
|
| 645 |
+
|
| 646 |
+
if st.session_state.metrics_done and st.session_state.metrics_signature == fetch_signature:
|
| 647 |
+
models_data = st.session_state.metrics_rows
|
| 648 |
+
elif should_run:
|
| 649 |
+
status_slot = st.empty()
|
| 650 |
+
progress = st.progress(0)
|
| 651 |
+
rows = []
|
| 652 |
+
with status_slot.status("Fetching reproduce.json β README fallback...", expanded=False) as status:
|
| 653 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as ex:
|
| 654 |
+
fut_to_id = {
|
| 655 |
+
ex.submit(cached_get_model_data, d["Model ID"], d["Likes"], d["Downloads"]): d["Model ID"]
|
| 656 |
+
for d in target_meta
|
| 657 |
+
}
|
| 658 |
+
done = 0
|
| 659 |
+
for fut in concurrent.futures.as_completed(fut_to_id):
|
| 660 |
+
try:
|
| 661 |
+
res = fut.result()
|
| 662 |
+
if res is not None:
|
| 663 |
+
rows.append(res)
|
| 664 |
+
except Exception as e:
|
| 665 |
+
logging.warning(f"Worker failed for {fut_to_id[fut]}: {e}")
|
| 666 |
+
done += 1
|
| 667 |
+
progress.progress(done / max(len(target_meta), 1))
|
| 668 |
+
status.update(label=f"Parsed {done}/{len(target_meta)} β {len(rows)} with metrics...")
|
| 669 |
+
status.update(label=f"Done: {len(rows)}/{len(target_meta)} with metrics", state="complete")
|
| 670 |
+
# Merge Created dates back in for display/filtering + popularity
|
| 671 |
+
# (likes/downloads live in popularity_store, NOT in the reasoning cache key)
|
| 672 |
+
created_map = {d["Model ID"]: d.get("Created") for d in target_meta}
|
| 673 |
+
meta_like_map = {d["Model ID"]: d for d in target_meta}
|
| 674 |
+
try:
|
| 675 |
+
_file_map = load_popularity()
|
| 676 |
+
except Exception:
|
| 677 |
+
_file_map = {}
|
| 678 |
+
for r in rows:
|
| 679 |
+
r.setdefault("Created", created_map.get(r.get("Model ID")))
|
| 680 |
+
mid = r.get("Model ID")
|
| 681 |
+
file_hit = (_file_map or {}).get(mid, {}) if mid else {}
|
| 682 |
+
meta_hit = meta_like_map.get(mid, {}) if mid else {}
|
| 683 |
+
if isinstance(file_hit, dict) and ("likes" in file_hit or "downloads" in file_hit):
|
| 684 |
+
try:
|
| 685 |
+
r["Likes"] = int(file_hit.get("likes", r.get("Likes", 0)) or 0)
|
| 686 |
+
except (TypeError, ValueError):
|
| 687 |
+
pass
|
| 688 |
+
try:
|
| 689 |
+
r["Downloads"] = int(file_hit.get("downloads", r.get("Downloads", 0)) or 0)
|
| 690 |
+
except (TypeError, ValueError):
|
| 691 |
+
pass
|
| 692 |
+
elif mid in meta_like_map:
|
| 693 |
+
try:
|
| 694 |
+
r["Likes"] = int(meta_hit.get("Likes", r.get("Likes", 0)) or 0)
|
| 695 |
+
except (TypeError, ValueError):
|
| 696 |
+
pass
|
| 697 |
+
try:
|
| 698 |
+
r["Downloads"] = int(meta_hit.get("Downloads", r.get("Downloads", 0)) or 0)
|
| 699 |
+
except (TypeError, ValueError):
|
| 700 |
+
pass
|
| 701 |
+
st.session_state.metrics_rows = rows
|
| 702 |
+
st.session_state.metrics_done = True
|
| 703 |
+
st.session_state.metrics_signature = fetch_signature
|
| 704 |
+
progress.empty()
|
| 705 |
+
status_slot.empty()
|
| 706 |
+
models_data = rows
|
| 707 |
+
st.rerun()
|
| 708 |
else:
|
| 709 |
+
st.info("Tick Auto-load or press Refresh to fetch KL/Refusals.")
|
| 710 |
+
st.stop()
|
| 711 |
+
|
| 712 |
+
if not models_data:
|
| 713 |
+
st.warning("No models found with the 'heretic' tag in this scope.")
|
| 714 |
+
st.stop()
|
| 715 |
+
|
| 716 |
+
df = pd.DataFrame(models_data)
|
| 717 |
+
# Make Model ID a clickable link
|
| 718 |
+
df["Model"] = df["URL"]
|
| 719 |
+
# Select and order columns for display (Created kept for filtering, shown in table too)
|
| 720 |
+
cols = ["Model", "KL Divergence", "Initial Refusals", "Refusals", "Refusal Rate (%)", "Likes", "Downloads"]
|
| 721 |
+
if "Created" in df.columns:
|
| 722 |
+
cols.append("Created")
|
| 723 |
+
display_df = df[cols]
|
| 724 |
+
|
| 725 |
+
# --- LATE metric filters (need parsed KL/Refusals) ---
|
| 726 |
+
# NOTE: no writes to st.session_state widget keys here β that was the
|
| 727 |
+
# StreamlitWidgetAlreadyInstantiatedError. Slider max is extended to fit the
|
| 728 |
+
# stored value instead of writing back.
|
| 729 |
+
kl_max_data = float(pd.to_numeric(display_df["KL Divergence"], errors="coerce").max() or 0.5)
|
| 730 |
+
_cur_kl = st.session_state.get("kl_range", FILTER_DEFAULTS["kl_range"])
|
| 731 |
+
try:
|
| 732 |
+
_cur_hi = float(_cur_kl[1])
|
| 733 |
+
except Exception:
|
| 734 |
+
_cur_hi = 0.5
|
| 735 |
+
kl_slider_max = max(0.5, kl_max_data, _cur_hi)
|
| 736 |
+
|
| 737 |
+
toolbar_sort, toolbar_reset = st.columns([3, 1])
|
| 738 |
+
with toolbar_sort:
|
| 739 |
+
sort_by = st.selectbox("Sort by", list(SORT_OPTIONS.keys()), key="sort_by")
|
| 740 |
+
with toolbar_reset:
|
| 741 |
+
st.write("")
|
| 742 |
+
st.button("Reset filters", key="reset_filters", use_container_width=True, on_click=_reset_filters)
|
| 743 |
+
|
| 744 |
+
with st.sidebar:
|
| 745 |
+
st.header("Metric filters")
|
| 746 |
+
kl_range = st.slider(
|
| 747 |
+
"KL Divergence",
|
| 748 |
+
min_value=0.0, max_value=float(kl_slider_max), step=0.001, format="%.3f",
|
| 749 |
+
key="kl_range",
|
| 750 |
)
|
| 751 |
+
refusal_range = st.slider(
|
| 752 |
+
"Refusal Rate (%)",
|
| 753 |
+
min_value=0.0, max_value=100.0, step=0.5, format="%.1f",
|
| 754 |
+
key="refusal_range",
|
| 755 |
+
)
|
| 756 |
+
min_likes = st.number_input("Min Likes", min_value=0, step=1, key="min_likes")
|
| 757 |
+
min_downloads = st.number_input("Min Downloads", min_value=0, step=10, key="min_downloads")
|
| 758 |
+
|
| 759 |
+
sync_filter_state_to_query_params()
|
| 760 |
+
|
| 761 |
+
query = st.session_state.get("query", "")
|
| 762 |
+
show_quants = st.session_state.get("show_quants", False)
|
| 763 |
+
date_range = st.session_state.get("date_range", "All time")
|
| 764 |
+
unknowns = st.session_state.get("unknowns", "Include unknowns")
|
| 765 |
+
filtered_df = apply_filters(display_df, query, kl_range, refusal_range, min_likes, min_downloads, show_quants, date_range, unknowns)
|
| 766 |
+
sort_col, ascending = SORT_OPTIONS.get(sort_by, ("KL Divergence", True))
|
| 767 |
+
if sort_col in filtered_df.columns:
|
| 768 |
+
filtered_df = filtered_df.sort_values(sort_col, ascending=ascending, kind="mergesort").reset_index(drop=True)
|
| 769 |
+
|
| 770 |
+
st.markdown(f"**Showing {len(filtered_df)} of {len(display_df)} models.**")
|
| 771 |
+
|
| 772 |
+
try:
|
| 773 |
+
import altair as alt
|
| 774 |
+
_ALT_OK = True
|
| 775 |
+
except ImportError:
|
| 776 |
+
alt = None # type: ignore
|
| 777 |
+
_ALT_OK = False
|
| 778 |
+
|
| 779 |
+
tab_table, tab_compare, tab_charts = st.tabs(["Table", "Compare", "Charts"])
|
| 780 |
+
|
| 781 |
+
# 1. Table β original config preserved
|
| 782 |
+
with tab_table:
|
| 783 |
+
if filtered_df.empty:
|
| 784 |
+
st.info("No models match the current filter.")
|
| 785 |
+
else:
|
| 786 |
+
st.dataframe(
|
| 787 |
+
filtered_df,
|
| 788 |
+
column_config={
|
| 789 |
+
"Model": st.column_config.LinkColumn("Model", display_text=r"https://huggingface\.co/(.*)"),
|
| 790 |
+
"KL Divergence": st.column_config.NumberColumn("KL Divergence", format="%.4f"),
|
| 791 |
+
"Initial Refusals": st.column_config.TextColumn("Initial Refusals"),
|
| 792 |
+
"Refusals": st.column_config.TextColumn("Refusals"),
|
| 793 |
+
"Refusal Rate (%)": st.column_config.NumberColumn("Refusal Rate (%)", format="%.2f%%"),
|
| 794 |
+
"Likes": st.column_config.NumberColumn("Likes"),
|
| 795 |
+
"Downloads": st.column_config.NumberColumn("Downloads"),
|
| 796 |
+
"Created": st.column_config.TextColumn("Created"),
|
| 797 |
+
},
|
| 798 |
+
hide_index=True,
|
| 799 |
+
width='stretch'
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
# 2. Compare β up to 4, transposed, NO winner trophies
|
| 803 |
+
with tab_compare:
|
| 804 |
+
if filtered_df.empty:
|
| 805 |
+
st.info("No models to compare β adjust filter.")
|
| 806 |
+
else:
|
| 807 |
+
st.caption(
|
| 808 |
+
"Grain of salt: a 0/x refusal alone proves little. "
|
| 809 |
+
"A 30/30 β 0/30 on a tiny sample is weaker than 95/100 β 2/100, "
|
| 810 |
+
"and zero refusals with high KL often means the weights got wrecked β "
|
| 811 |
+
"uncensored but stupid. Read Refusal **with** KL + sample size."
|
| 812 |
+
)
|
| 813 |
+
options = filtered_df["Model"].tolist()
|
| 814 |
+
selected = st.multiselect(
|
| 815 |
+
"Select up to 4 models to compare",
|
| 816 |
+
options=options,
|
| 817 |
+
default=options[:2] if len(options) >= 2 else options,
|
| 818 |
+
max_selections=4,
|
| 819 |
+
format_func=_short,
|
| 820 |
+
)
|
| 821 |
+
if len(selected) < 2:
|
| 822 |
+
st.info("Select at least 2 models to compare.")
|
| 823 |
+
else:
|
| 824 |
+
comp = filtered_df[filtered_df["Model"].isin(selected)].set_index("Model").reindex(selected)
|
| 825 |
+
comp["KL Divergence"] = pd.to_numeric(comp["KL Divergence"], errors="coerce")
|
| 826 |
+
comp["Refusal Rate (%)"] = pd.to_numeric(comp["Refusal Rate (%)"], errors="coerce")
|
| 827 |
+
# Per-model honest summary (no best_kl / best_ref verdicts)
|
| 828 |
+
for m in selected:
|
| 829 |
+
row = comp.loc[m]
|
| 830 |
+
rn, rd = _parse_fraction(row.get("Refusals"))
|
| 831 |
+
inn, ind = _parse_fraction(row.get("Initial Refusals"))
|
| 832 |
+
note = _compare_note(row.get("KL Divergence"), rn, rd, inn, ind)
|
| 833 |
+
band = _kl_band(row.get("KL Divergence"))
|
| 834 |
+
st.info(f"**{_short(m)}** β KL {band} | {note}")
|
| 835 |
+
disp = comp.T.copy()
|
| 836 |
+
disp.columns = [_short(c) for c in disp.columns]
|
| 837 |
+
# Extra derived rows for honest comparison
|
| 838 |
+
try:
|
| 839 |
+
kl_vals = pd.to_numeric(comp["KL Divergence"], errors="coerce")
|
| 840 |
+
disp.loc["KL band"] = [_kl_band(v) for v in kl_vals.reindex(selected).tolist()]
|
| 841 |
+
except Exception:
|
| 842 |
+
pass
|
| 843 |
+
try:
|
| 844 |
+
drops = []
|
| 845 |
+
for m in selected:
|
| 846 |
+
rn, rd = _parse_fraction(comp.loc[m].get("Refusals"))
|
| 847 |
+
inn, ind = _parse_fraction(comp.loc[m].get("Initial Refusals"))
|
| 848 |
+
drops.append(str(inn - rn) if inn is not None and rn is not None else "β")
|
| 849 |
+
disp.loc["Refusal drop (base β final)"] = drops
|
| 850 |
+
except Exception:
|
| 851 |
+
pass
|
| 852 |
+
try:
|
| 853 |
+
ns = []
|
| 854 |
+
for m in selected:
|
| 855 |
+
_, rd = _parse_fraction(comp.loc[m].get("Refusals"))
|
| 856 |
+
ns.append(str(rd) if rd is not None else "β")
|
| 857 |
+
disp.loc["Sample size (n prompts)"] = ns
|
| 858 |
+
except Exception:
|
| 859 |
+
pass
|
| 860 |
+
for metric in list(disp.index):
|
| 861 |
+
for col_url, col_short in zip(selected, disp.columns):
|
| 862 |
+
v = comp.loc[col_url, metric] if metric in comp.columns else disp.loc[metric, col_short]
|
| 863 |
+
if pd.isna(v):
|
| 864 |
+
disp.loc[metric, col_short] = "β"
|
| 865 |
+
elif metric == "KL Divergence":
|
| 866 |
+
try:
|
| 867 |
+
disp.loc[metric, col_short] = f"{float(v):.4f}"
|
| 868 |
+
except (TypeError, ValueError):
|
| 869 |
+
disp.loc[metric, col_short] = str(v)
|
| 870 |
+
elif metric == "Refusal Rate (%)":
|
| 871 |
+
try:
|
| 872 |
+
disp.loc[metric, col_short] = f"{float(v):.2f}%"
|
| 873 |
+
except (TypeError, ValueError):
|
| 874 |
+
disp.loc[metric, col_short] = str(v)
|
| 875 |
+
elif metric in ("Likes", "Downloads"):
|
| 876 |
+
try:
|
| 877 |
+
disp.loc[metric, col_short] = f"{int(float(v)):,}"
|
| 878 |
+
except Exception:
|
| 879 |
+
disp.loc[metric, col_short] = str(v)
|
| 880 |
+
else:
|
| 881 |
+
disp.loc[metric, col_short] = str(v) if v is not None else "β"
|
| 882 |
+
disp.index.name = "Metric"
|
| 883 |
+
st.table(disp)
|
| 884 |
+
|
| 885 |
+
# 3. Charts β altair, NaN/empty safe
|
| 886 |
+
with tab_charts:
|
| 887 |
+
if not _ALT_OK:
|
| 888 |
+
st.warning("`altair` is not installed. Add `altair` to requirements.txt and reboot.")
|
| 889 |
+
elif filtered_df.empty:
|
| 890 |
+
st.info("No data to chart β adjust filter.")
|
| 891 |
+
else:
|
| 892 |
+
chart_df = filtered_df.copy()
|
| 893 |
+
chart_df["Short"] = chart_df["Model"].astype(str).str.split("huggingface.co/").str[-1]
|
| 894 |
+
chart_df["KL Divergence"] = pd.to_numeric(chart_df["KL Divergence"], errors="coerce")
|
| 895 |
+
chart_df["Refusal Rate (%)"] = pd.to_numeric(chart_df["Refusal Rate (%)"], errors="coerce")
|
| 896 |
+
chart_df["Likes"] = pd.to_numeric(chart_df["Likes"], errors="coerce").fillna(0)
|
| 897 |
+
chart_df["Downloads"] = pd.to_numeric(chart_df["Downloads"], errors="coerce").fillna(0)
|
| 898 |
+
scatter_df = chart_df.dropna(subset=["KL Divergence", "Refusal Rate (%)"])
|
| 899 |
+
if scatter_df.empty:
|
| 900 |
+
st.info("No valid KL / Refusal Rate values to plot.")
|
| 901 |
+
else:
|
| 902 |
+
st.subheader("KL (x) vs Refusal Rate (y), size=Downloads")
|
| 903 |
+
scatter = (
|
| 904 |
+
alt.Chart(scatter_df)
|
| 905 |
+
.mark_circle(opacity=0.7)
|
| 906 |
+
.encode(
|
| 907 |
+
x=alt.X("KL Divergence:Q", scale=alt.Scale(zero=False)),
|
| 908 |
+
y=alt.Y("Refusal Rate (%):Q", scale=alt.Scale(zero=False)),
|
| 909 |
+
size=alt.Size("Downloads:Q", scale=alt.Scale(range=[20, 500])),
|
| 910 |
+
tooltip=["Short:N", "KL Divergence:Q", "Refusal Rate (%):Q", "Likes:Q", "Downloads:Q"],
|
| 911 |
+
)
|
| 912 |
+
.interactive()
|
| 913 |
+
)
|
| 914 |
+
st.altair_chart(scatter, use_container_width=True)
|
| 915 |
+
st.subheader("Refusal Rate per model (top 20 by Likes)")
|
| 916 |
+
top20 = chart_df.nlargest(20, "Likes").sort_values("Refusal Rate (%)", ascending=True)
|
| 917 |
+
top20 = top20.dropna(subset=["Refusal Rate (%)"])
|
| 918 |
+
if top20.empty:
|
| 919 |
+
st.info("No valid Refusal Rate values for bar chart.")
|
| 920 |
+
else:
|
| 921 |
+
bars = (
|
| 922 |
+
alt.Chart(top20)
|
| 923 |
+
.mark_bar()
|
| 924 |
+
.encode(
|
| 925 |
+
x=alt.X("Refusal Rate (%):Q"),
|
| 926 |
+
y=alt.Y("Short:N", sort="-x", title="Model"),
|
| 927 |
+
tooltip=["Short:N", "Refusal Rate (%):Q", "KL Divergence:Q", "Likes:Q", "Downloads:Q"],
|
| 928 |
+
)
|
| 929 |
+
)
|
| 930 |
+
st.altair_chart(bars, use_container_width=True)
|
| 931 |
+
st.subheader("Distribution of KL Divergence")
|
| 932 |
+
kl_df = chart_df.dropna(subset=["KL Divergence"])
|
| 933 |
+
if kl_df.empty:
|
| 934 |
+
st.info("No valid KL values for histogram.")
|
| 935 |
+
else:
|
| 936 |
+
hist = (
|
| 937 |
+
alt.Chart(kl_df)
|
| 938 |
+
.mark_bar()
|
| 939 |
+
.encode(
|
| 940 |
+
x=alt.X("KL Divergence:Q", bin=alt.Bin(maxbins=20)),
|
| 941 |
+
y=alt.Y("count():Q", title="Models"),
|
| 942 |
+
tooltip=[alt.Tooltip("count():Q", title="Models")],
|
| 943 |
+
)
|
| 944 |
+
)
|
| 945 |
+
st.altair_chart(hist, use_container_width=True)
|