| """ |
| 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, |
| ) |
|
|
| |
| BENCHMARK_DATASET_ID = "Sefaria/Rabbinic-Hebrew-English-Pairs" |
|
|
| |
| _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") |
| |
| _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() |
| |
| |
| 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) |
| """ |
| |
| def status_update(msg): |
| return (msg, gr.update(), gr.update()) |
| |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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: |
| |
| 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 |
| |
| |
| try: |
| results = None |
| for item in evaluate_model_streaming(model, benchmark, batch_size=32): |
| if isinstance(item, str): |
| |
| yield status_update(item) |
| else: |
| |
| 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) |
| |
| |
| 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:,} | |
| """ |
| |
| |
| 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 |
| |
| |
| 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), |
| ) |
|
|
|
|
| |
| def create_app(): |
| """Create and return the Gradio app.""" |
| |
| |
| model_choices = [] |
| |
| |
| for model_id, info in CURATED_MODELS.items(): |
| model_choices.append((f"π₯οΈ {info['name']}", model_id)) |
| |
| |
| for model_id, info in API_MODELS.items(): |
| model_choices.append((f"π {info['name']}", model_id)) |
| |
| |
| model_choices.append(("βοΈ Custom Model (enter ID below)", "custom")) |
| |
| |
| 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). |
| """) |
| |
| |
| 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 |
|
|
|
|
| |
| if __name__ == "__main__": |
| app = create_app() |
| app.launch() |
|
|
|
|