import time
import pandas as pd
import streamlit as st
from datasets import load_dataset
from transformers import AutoTokenizer
try:
from nomeda import NomedaMSATokenizer
except ImportError:
NomedaMSATokenizer = None
# -- Page Config & CSS --
st.set_page_config(
page_title="MSA Tokenizer Arena 🏆",
page_icon="🏆",
layout="wide",
)
st.markdown(
"""
""",
unsafe_allow_html=True,
)
# -- Title --
st.markdown("
🏆 MSA Tokenizer Arena
", unsafe_allow_html=True)
st.markdown(
"Evaluate and compare tokenizers on Modern Standard Arabic (MSA) datasets.
",
unsafe_allow_html=True,
)
# -- State Management --
if "leaderboard" not in st.session_state:
st.session_state.leaderboard = pd.DataFrame()
if "tokenizer_list" not in st.session_state:
st.session_state.tokenizer_list = [
"nomeda-lab/Nomeda-MSA-64Kv1",
"aubmindlab/bert-base-arabertv02",
"core42/jais-13b-chat",
]
# -- Configuration Section --
col1, col2 = st.columns(2)
with col1:
with st.expander("📂 Dataset Configuration", expanded=True):
data_source = st.radio(
"Dataset Source",
["Hugging Face Hub", "Upload File (CSV/JSONL/Parquet)"],
horizontal=True,
)
dataset_name = None
uploaded_file = None
if data_source == "Hugging Face Hub":
dataset_name = st.text_input(
"🤗 Hugging Face Dataset Path", value="abucoderr/arabic_news_dataset"
)
split = st.text_input("Dataset Split", value="train")
else:
uploaded_file = st.file_uploader(
"Upload File", type=["csv", "jsonl", "parquet", "json"]
)
c1, c2 = st.columns(2)
text_column = c1.text_input(
"Text Column Name",
value="text",
help="The name of the column containing the text data.",
)
num_samples = c2.number_input(
"Samples to Evaluate", min_value=10, max_value=50000, value=1000, step=100
)
with col2:
with st.expander("🤖 Tokenizers Configuration", expanded=True):
st.markdown("Add Hugging Face tokenizer IDs to evaluate.")
# Add new tokenizer using a form to ensure reliable interactions
with st.form("add_tokenizer_form", clear_on_submit=True):
add_col1, add_col2 = st.columns([3, 1])
new_tok = add_col1.text_input(
"Tokenizer ID",
placeholder="e.g., openai/clip-vit-base-patch32",
label_visibility="collapsed",
)
submitted = add_col2.form_submit_button("➕ Add")
if submitted and new_tok:
tok_clean = new_tok.strip()
if tok_clean and tok_clean not in st.session_state.tokenizer_list:
st.session_state.tokenizer_list.append(tok_clean)
st.rerun()
st.markdown("---")
st.markdown("**Selected Tokenizers:**")
# Display list with remove buttons
if not st.session_state.tokenizer_list:
st.info("No tokenizers added yet.")
for i, tok in enumerate(st.session_state.tokenizer_list):
tc1, tc2 = st.columns([5, 1])
tc1.markdown(
f"{tok}
", unsafe_allow_html=True
)
if tc2.button("❌", key=f"del_{i}", help="Remove tokenizer"):
st.session_state.tokenizer_list.pop(i)
st.rerun()
st.markdown("
", unsafe_allow_html=True)
# Run Button centered
run_col1, run_col2, run_col3 = st.columns([1, 2, 1])
with run_col2:
run_eval = st.button("🚀 Run Evaluation", type="primary")
st.markdown("---")
# -- Main Logic --
@st.cache_resource
def load_hf_tokenizer(model_id):
try:
# Custom logic for Nomeda tokenizer loading
if "nomeda-msa" in model_id.lower() or "nomeda-lab" in model_id.lower():
if NomedaMSATokenizer is not None:
try:
# In case the library supports it directly
return NomedaMSATokenizer.from_pretrained(model_id)
except Exception:
# Fallback to downloading the tsv if needed
from huggingface_hub import hf_hub_download
tsv_path = hf_hub_download(
repo_id=model_id, filename=model_id.split("/")[-1] + ".tsv"
)
return NomedaMSATokenizer.from_tsv(tsv_path)
else:
return "The 'nomeda' package is required but not installed."
return AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
except Exception as e:
return str(e)
@st.cache_data
def get_data(file_or_name, source_type, col_name, split, samples):
try:
if source_type == "Hugging Face Hub":
dataset = load_dataset(file_or_name, split=f"{split}[:{samples}]")
if col_name not in dataset.column_names:
return (
None,
f"Column '{col_name}' not found. Available columns: {dataset.column_names}",
)
texts = [str(t) for t in dataset[col_name] if t is not None]
else:
if file_or_name.name.endswith(".csv"):
df = pd.read_csv(file_or_name, nrows=samples)
elif file_or_name.name.endswith(".parquet"):
df = pd.read_parquet(file_or_name)
df = df.head(samples)
elif file_or_name.name.endswith(".jsonl") or file_or_name.name.endswith(
".json"
):
df = pd.read_json(file_or_name, lines=True, nrows=samples)
else:
return None, "Unsupported file format."
if col_name not in df.columns:
return (
None,
f"Column '{col_name}' not found. Available columns: {list(df.columns)}",
)
texts = df[col_name].dropna().astype(str).tolist()
return texts, None
except Exception as e:
return None, str(e)
if run_eval:
if not st.session_state.tokenizer_list:
st.error("Please add at least one tokenizer.")
else:
with st.status("Evaluating Tokenizers...", expanded=True) as status:
st.write("Loading dataset...")
if data_source == "Hugging Face Hub":
if not dataset_name:
status.update(label="Failed", state="error")
st.error("Please provide a dataset name.")
st.stop()
texts, data_err = get_data(
dataset_name, data_source, text_column, split, num_samples
)
else:
if uploaded_file is None:
status.update(label="Failed", state="error")
st.error("Please upload a file.")
st.stop()
texts, data_err = get_data(
uploaded_file, data_source, text_column, None, num_samples
)
if data_err:
status.update(label="Failed", state="error")
st.error(f"Error loading dataset: {data_err}")
st.stop()
total_chars = sum(len(t) for t in texts)
st.write(
f"✅ Loaded {len(texts)} samples. Total characters: **{total_chars:,}**"
)
results = []
# Progress bar
progress_bar = st.progress(0)
total_toks = len(st.session_state.tokenizer_list)
for idx, model_id in enumerate(st.session_state.tokenizer_list):
st.write(f"🔄 Evaluating `{model_id}`...")
tok = load_hf_tokenizer(model_id)
if isinstance(tok, str):
st.warning(f"Failed to load `{model_id}`: {tok}")
progress_bar.progress((idx + 1) / total_toks)
continue
vocab_size = (
len(tok)
if hasattr(tok, "__len__")
else getattr(tok, "vocab_size", "Unknown")
)
start_time = time.time()
if type(tok).__name__ == "NomedaMSATokenizer":
# Custom handling for Nomeda tokenizer
total_tokens = 0
for t in texts:
encoded = tok.encode(t)
total_tokens += len(encoded.ids)
else:
encoded = tok(texts, add_special_tokens=False)
total_tokens = sum(len(ids) for ids in encoded["input_ids"])
end_time = time.time()
time_taken = end_time - start_time
tokens_per_sec = total_tokens / time_taken if time_taken > 0 else 0
compression_ratio = (
total_chars / total_tokens if total_tokens > 0 else 0
)
results.append(
{
"Tokenizer": model_id,
"Compression Ratio (Chars/Token) ⬆️": round(
compression_ratio, 2
),
"Total Tokens ⬇️": total_tokens,
"Speed (Tokens/sec) ⬆️": round(tokens_per_sec, 0),
"Vocab Size": vocab_size,
"Time Taken (s)": round(time_taken, 2),
}
)
progress_bar.progress((idx + 1) / total_toks)
if results:
df = pd.DataFrame(results)
df = df.sort_values(
by="Compression Ratio (Chars/Token) ⬆️", ascending=False
).reset_index(drop=True)
df.index = df.index + 1
df.index.name = "Rank"
st.session_state.leaderboard = df
status.update(
label="Evaluation Complete! 🎉", state="complete", expanded=False
)
# -- Leaderboard Display --
if not st.session_state.leaderboard.empty:
st.subheader("📊 Arena Leaderboard")
st.markdown(
"Higher **Compression Ratio** means the tokenizer is more efficient (requires fewer tokens to represent the same text)."
)
def highlight_max(s, props=""):
return [props if v == s.max() else "" for v in s]
def highlight_min(s, props=""):
return [props if v == s.min() else "" for v in s]
styled_df = (
st.session_state.leaderboard.style.apply(
highlight_max,
props="background-color: #dcfce7; color: #166534; font-weight: bold;",
subset=["Compression Ratio (Chars/Token) ⬆️", "Speed (Tokens/sec) ⬆️"],
)
.apply(
highlight_min,
props="background-color: #dcfce7; color: #166534; font-weight: bold;",
subset=["Total Tokens ⬇️"],
)
.format(
{
"Total Tokens ⬇️": "{:,}",
"Speed (Tokens/sec) ⬆️": "{:,.0f}",
"Compression Ratio (Chars/Token) ⬆️": "{:.2f}",
}
)
)
st.dataframe(styled_df, use_container_width=True, height=400)
col1, col2, col3 = st.columns([1, 1, 1])
with col2:
csv = st.session_state.leaderboard.to_csv().encode("utf-8")
st.download_button(
label="📥 Download Results (CSV)",
data=csv,
file_name="tokenizer_arena_results.csv",
mime="text/csv",
use_container_width=True,
)