Lev Israel
Setup HF space
112e258
Raw
History Blame
16.2 kB
"""
Gradio interface for Rabbinic Hebrew/Aramaic Embedding Evaluation.
A Hugging Face Space for evaluating embedding models on cross-lingual
retrieval between Hebrew/Aramaic source texts and English translations.
"""
import os
from datetime import datetime
import gradio as gr
import pandas as pd
import plotly.graph_objects as go
from data_loader import load_benchmark_dataset, get_benchmark_stats
from models import (
CURATED_MODELS,
API_MODELS,
ALL_MODELS,
get_curated_model_choices,
get_api_model_choices,
get_all_model_choices,
load_model,
validate_model_id,
is_api_model,
requires_api_key,
api_key_optional,
get_api_key_type,
get_api_key_env_var,
)
from evaluation import (
EvaluationResults,
evaluate_model,
evaluate_model_streaming,
compute_similarity_matrix,
get_rank_distribution,
)
from leaderboard import (
load_leaderboard as load_leaderboard_from_hub,
add_result as add_result_to_hub,
)
# HuggingFace Dataset ID for benchmark data
BENCHMARK_DATASET_ID = "Sefaria/Rabbinic-Hebrew-English-Pairs"
# Global state
_benchmark_data = None
def load_benchmark():
"""Load benchmark data from HuggingFace Hub, with fallback to sample data."""
global _benchmark_data
if _benchmark_data is not None:
return _benchmark_data
try:
_benchmark_data = load_benchmark_dataset(BENCHMARK_DATASET_ID)
print(f"Loaded {len(_benchmark_data)} benchmark pairs from {BENCHMARK_DATASET_ID}")
except Exception as e:
print(f"Failed to load benchmark: {e}")
print("Using sample data for testing")
# Create minimal sample data for testing
_benchmark_data = [
{
"ref": "Sample.1",
"he": "בראשיΧͺ ברא ΧΧœΧ”Χ™Χ אΧͺ Χ”Χ©ΧžΧ™Χ ואΧͺ הארΧ₯",
"en": "In the beginning God created the heaven and the earth",
"category": "Sample",
},
{
"ref": "Sample.2",
"he": "והארΧ₯ Χ”Χ™ΧͺΧ” ΧͺΧ”Χ• Χ•Χ‘Χ”Χ• Χ•Χ—Χ©Χš גל Χ€Χ Χ™ Χͺהום",
"en": "And the earth was without form, and void; and darkness was upon the face of the deep",
"category": "Sample",
},
]
return _benchmark_data
def load_leaderboard():
"""Load leaderboard from HuggingFace Hub."""
return load_leaderboard_from_hub()
def add_to_leaderboard(results: EvaluationResults):
"""Add evaluation results to leaderboard on HuggingFace Hub."""
entry = results.to_dict()
entry["timestamp"] = datetime.now().isoformat()
# Add to Hub (handles deduplication and sorting internally)
success = add_result_to_hub(entry)
if not success:
print("Note: Results saved locally but not persisted to Hub (no HF_TOKEN)")
def format_leaderboard_df():
"""Format leaderboard as pandas DataFrame for display."""
leaderboard = load_leaderboard()
if not leaderboard:
return pd.DataFrame(columns=[
"#", "Model", "MRR", "R@1", "R@5", "R@10",
"Bitext", "TrueSim", "RandSim", "N"
])
rows = []
for i, entry in enumerate(leaderboard, 1):
rows.append({
"#": i,
"Model": entry.get("model_name", entry["model_id"]),
"MRR": f"{entry['mrr']:.3f}",
"R@1": f"{entry['recall_at_1']:.1%}",
"R@5": f"{entry['recall_at_5']:.1%}",
"R@10": f"{entry['recall_at_10']:.1%}",
"Bitext": f"{entry['bitext_accuracy']:.1%}",
"TrueSim": f"{entry['avg_true_pair_similarity']:.3f}",
"RandSim": f"{entry['avg_random_pair_similarity']:.3f}",
"N": entry["num_pairs"],
})
return pd.DataFrame(rows)
def run_evaluation(
model_choice: str,
custom_model_id: str,
api_key: str,
max_pairs: int,
):
"""
Run evaluation for the selected model (generator for streaming status updates).
Args:
model_choice: Selected curated model or "custom"
custom_model_id: Custom model ID if selected
api_key: API key for API-based models
max_pairs: Maximum pairs to evaluate
Yields:
Tuples of (status, results, leaderboard)
"""
# Helper to yield status updates
def status_update(msg):
return (msg, gr.update(), gr.update())
# Determine which model to use
if model_choice == "custom":
model_id = custom_model_id.strip()
is_valid, error = validate_model_id(model_id)
if not is_valid:
yield (
f"❌ {error}",
f"❌ Invalid model ID: {error}",
format_leaderboard_df(),
)
return
else:
model_id = model_choice
# Check if API key is required but not provided
if requires_api_key(model_id):
api_key = api_key.strip() if api_key else ""
env_var = get_api_key_env_var(model_id)
key_type = get_api_key_type(model_id)
# Skip API key check for models that support Application Default Credentials
if not api_key and not os.environ.get(env_var) and not api_key_optional(model_id):
yield (
"❌ API key required",
f"❌ API key required for {model_id}. Please enter your {key_type.upper()} API key or set the {env_var} environment variable.",
format_leaderboard_df(),
)
return
yield status_update(f"⏳ Loading benchmark data...")
benchmark = load_benchmark()
if max_pairs and max_pairs < len(benchmark):
benchmark = benchmark[:max_pairs]
yield status_update(f"⏳ Loading model: {model_id}...")
try:
# Pass API key for API-based models
model = load_model(model_id, api_key=api_key if api_key else None)
except Exception as e:
yield (
"❌ Model load failed",
f"❌ Failed to load model: {str(e)}",
format_leaderboard_df(),
)
return
# Stream progress updates during evaluation
try:
results = None
for item in evaluate_model_streaming(model, benchmark, batch_size=32):
if isinstance(item, str):
# Progress update
yield status_update(item)
else:
# Final results
results = item
except Exception as e:
yield (
"❌ Evaluation failed",
f"❌ Evaluation failed: {str(e)}",
format_leaderboard_df(),
)
return
yield status_update("⏳ Saving results...")
add_to_leaderboard(results)
# Format results summary
summary = f"""## Results for {results.model_name}
| Metric | Value |
|--------|-------|
| **MRR** | {results.mrr:.4f} |
| **Recall@1** | {results.recall_at_1:.1%} |
| **Recall@5** | {results.recall_at_5:.1%} |
| **Recall@10** | {results.recall_at_10:.1%} |
| **Bitext Accuracy** | {results.bitext_accuracy:.1%} |
| **Avg True Pair Sim** | {results.avg_true_pair_similarity:.4f} |
| **Avg Random Pair Sim** | {results.avg_random_pair_similarity:.4f} |
| **Pairs Evaluated** | {results.num_pairs:,} |
"""
# Final yield with all results (clear status)
yield (
"βœ… Complete!",
summary,
format_leaderboard_df(),
)
def create_leaderboard_comparison():
"""Create comparison chart of all models on leaderboard."""
leaderboard = load_leaderboard()
if len(leaderboard) < 2:
return None
models = [e.get("model_name", e["model_id"]) for e in leaderboard]
mrr = [e["mrr"] for e in leaderboard]
r1 = [e["recall_at_1"] for e in leaderboard]
r5 = [e["recall_at_5"] for e in leaderboard]
r10 = [e["recall_at_10"] for e in leaderboard]
bitext = [e["bitext_accuracy"] for e in leaderboard]
fig = go.Figure()
fig.add_trace(go.Bar(name="MRR", x=models, y=mrr, marker_color="#2E86AB"))
fig.add_trace(go.Bar(name="R@1", x=models, y=r1, marker_color="#A23B72"))
fig.add_trace(go.Bar(name="R@5", x=models, y=r5, marker_color="#F18F01"))
fig.add_trace(go.Bar(name="R@10", x=models, y=r10, marker_color="#C73E1D"))
fig.add_trace(go.Bar(name="Bitext Acc", x=models, y=bitext, marker_color="#6B5B95"))
fig.update_layout(
title="Model Comparison",
yaxis_title="Score",
yaxis_range=[0, 1],
barmode="group",
template="plotly_white",
height=400,
)
return fig
def update_model_inputs_visibility(choice):
"""Show/hide custom model input and API key based on selection."""
show_custom = (choice == "custom")
show_api_key = requires_api_key(choice) if choice != "custom" else False
# Update API key label based on model type
if show_api_key:
key_type = get_api_key_type(choice)
env_var = get_api_key_env_var(choice)
is_optional = api_key_optional(choice)
if key_type == "voyage":
label = "Voyage AI API Key"
placeholder = f"Enter your Voyage AI API key (or set {env_var} env var)"
elif key_type == "gemini":
label = "Gemini API Key (optional if using gcloud)"
placeholder = f"Leave blank if using gcloud ADC, or enter API key / set {env_var}"
else:
label = "OpenAI API Key"
placeholder = f"Enter your OpenAI API key (or set {env_var} env var)"
return (
gr.update(visible=show_custom),
gr.update(visible=show_api_key, label=label, placeholder=placeholder),
)
return (
gr.update(visible=show_custom),
gr.update(visible=show_api_key),
)
# Build the Gradio interface
def create_app():
"""Create and return the Gradio app."""
# Get all model choices - local models first, then API models
model_choices = []
# Local models
for model_id, info in CURATED_MODELS.items():
model_choices.append((f"πŸ–₯️ {info['name']}", model_id))
# API models
for model_id, info in API_MODELS.items():
model_choices.append((f"🌐 {info['name']}", model_id))
# Custom option
model_choices.append(("βš™οΈ Custom Model (enter ID below)", "custom"))
# Load initial data
load_benchmark()
load_leaderboard()
benchmark_stats = get_benchmark_stats(_benchmark_data) if _benchmark_data else {}
with gr.Blocks(
title="Rabbinic Embedding Benchmark",
theme=gr.themes.Soft(
primary_hue="blue",
secondary_hue="orange",
font=gr.themes.GoogleFont("Source Sans Pro"),
),
css="""
.main-header {
text-align: center;
margin-bottom: 1rem;
}
.stats-box {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem;
border-radius: 8px;
margin: 0.5rem 0;
}
""",
) as app:
gr.Markdown(
"""
# πŸ“š Rabbinic Hebrew/Aramaic Embedding Benchmark
Evaluate embedding models on cross-lingual retrieval between Hebrew/Aramaic
source texts and their English translations from Sefaria.
**How it works:** Given a Hebrew/Aramaic text, can the model find its correct
English translation from a pool of candidates? Models that excel at this task
produce high-quality embeddings for Rabbinic literature.
""",
elem_classes=["main-header"],
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown(f"""
### πŸ“Š Benchmark Stats
- **Total Pairs:** {benchmark_stats.get('total_pairs', 'N/A'):,}
- **Categories:** {len(benchmark_stats.get('categories', {}))}
- **Avg Hebrew Length:** {benchmark_stats.get('avg_he_length', 0):.0f} chars
""")
with gr.Column(scale=1):
gr.Markdown("""
### πŸ“ Metrics
- **MRR:** Mean Reciprocal Rank
- **R@k:** Recall at k (correct in top k)
- **Bitext Acc:** True vs random pair classification
""")
gr.Markdown("---")
with gr.Tabs():
with gr.TabItem("πŸ”¬ Evaluate Model"):
with gr.Row():
with gr.Column(scale=2):
model_dropdown = gr.Dropdown(
choices=model_choices,
value=model_choices[0][1],
label="Select Model",
info="Choose a curated model or enter a custom Hugging Face model ID",
)
custom_model_input = gr.Textbox(
label="Custom Model ID",
placeholder="e.g., organization/model-name",
visible=False,
)
api_key_input = gr.Textbox(
label="API Key",
placeholder="Enter your API key (or set appropriate env var)",
type="password",
visible=False,
info="Required for API-based models (OpenAI, Voyage AI). Your key is not stored.",
)
total_pairs = benchmark_stats.get('total_pairs', 1000)
max_pairs_slider = gr.Slider(
minimum=100,
maximum=total_pairs,
value=total_pairs,
step=100,
label="Max Pairs to Evaluate",
info="Use fewer pairs for faster evaluation",
)
with gr.Column(scale=3):
evaluate_btn = gr.Button(
"πŸš€ Run Evaluation",
variant="primary",
size="lg",
)
status_text = gr.Markdown("")
results_markdown = gr.Markdown("")
with gr.TabItem("πŸ† Leaderboard"):
leaderboard_table = gr.Dataframe(
value=format_leaderboard_df(),
label="Model Rankings",
interactive=False,
)
refresh_btn = gr.Button("πŸ”„ Refresh Leaderboard")
comparison_plot = gr.Plot(label="Model Comparison")
gr.Markdown("""
---
### About
This benchmark evaluates embedding models for Rabbinic Hebrew and Aramaic texts using
cross-lingual retrieval.
All texts and translations sourced from [Sefaria](https://www.sefaria.org).
""")
# Event handlers
model_dropdown.change(
fn=update_model_inputs_visibility,
inputs=[model_dropdown],
outputs=[custom_model_input, api_key_input],
)
evaluate_btn.click(
fn=run_evaluation,
inputs=[model_dropdown, custom_model_input, api_key_input, max_pairs_slider],
outputs=[status_text, results_markdown, leaderboard_table],
show_progress="hidden",
)
refresh_btn.click(
fn=lambda: (format_leaderboard_df(), create_leaderboard_comparison()),
outputs=[leaderboard_table, comparison_plot],
)
return app
# Main entry point
if __name__ == "__main__":
app = create_app()
app.launch()