app
Browse files- README.md +7 -5
- __pycache__/app.cpython-311.pyc +0 -0
- __pycache__/db.cpython-311.pyc +0 -0
- __pycache__/ocr_models.cpython-311.pyc +0 -0
- __pycache__/storage.cpython-311.pyc +0 -0
- __pycache__/ui_helpers.cpython-311.pyc +0 -0
- app.py +543 -0
- db.py +262 -0
- ocr_models.py +191 -0
- ocr_votes.sql +25 -0
- requirements.txt +10 -0
- storage.py +212 -0
- ui_helpers.py +179 -0
README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: OCR Arena
|
| 3 |
+
emoji: ⚔️
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.38.2
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
hf_oauth: true
|
| 11 |
+
short_description: A leaderboard for OCR algorithms
|
| 12 |
---
|
| 13 |
|
| 14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (22.4 kB). View file
|
|
|
__pycache__/db.cpython-311.pyc
ADDED
|
Binary file (11.2 kB). View file
|
|
|
__pycache__/ocr_models.cpython-311.pyc
ADDED
|
Binary file (8.78 kB). View file
|
|
|
__pycache__/storage.cpython-311.pyc
ADDED
|
Binary file (9.58 kB). View file
|
|
|
__pycache__/ui_helpers.cpython-311.pyc
ADDED
|
Binary file (8.19 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OCR Arena - Main Application
|
| 3 |
+
A Gradio web application for comparing OCR results from different AI models.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import logging
|
| 8 |
+
import os
|
| 9 |
+
import datetime
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
from storage import upload_file_to_bucket
|
| 12 |
+
from db import add_vote, get_all_votes, calculate_elo_ratings_from_votes
|
| 13 |
+
from ocr_models import process_model_ocr, initialize_gemini, initialize_mistral, initialize_openai
|
| 14 |
+
from ui_helpers import (
|
| 15 |
+
get_model_display_name, select_random_models, format_votes_table,
|
| 16 |
+
format_elo_leaderboard
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Load environment variables
|
| 20 |
+
load_dotenv()
|
| 21 |
+
|
| 22 |
+
# Configure logging
|
| 23 |
+
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
# Initialize API keys and models
|
| 27 |
+
initialize_gemini()
|
| 28 |
+
initialize_mistral()
|
| 29 |
+
initialize_openai()
|
| 30 |
+
|
| 31 |
+
# Get Supabase credentials
|
| 32 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
| 33 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
| 34 |
+
|
| 35 |
+
# Global variables to store current OCR results and image URL
|
| 36 |
+
current_gemini_output = ""
|
| 37 |
+
current_mistral_output = ""
|
| 38 |
+
current_openai_output = ""
|
| 39 |
+
current_gpt5_output = ""
|
| 40 |
+
current_gpt5_output = ""
|
| 41 |
+
current_image_url = ""
|
| 42 |
+
current_voted_users = set() # Track users who have already voted
|
| 43 |
+
current_model_a = "" # Store which model was selected as model A
|
| 44 |
+
current_model_b = "" # Store which model was selected as model B
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def get_default_username(profile: gr.OAuthProfile | None) -> str:
|
| 48 |
+
"""Returns the username if the user is logged in, or an empty string if not logged in."""
|
| 49 |
+
if profile is None:
|
| 50 |
+
return ""
|
| 51 |
+
return profile.username
|
| 52 |
+
|
| 53 |
+
def get_current_username(profile_or_username) -> str:
|
| 54 |
+
"""Returns the username from login or "Anonymous" if not logged in."""
|
| 55 |
+
# Check if profile_or_username is a profile object with username attribute
|
| 56 |
+
if hasattr(profile_or_username, 'username') and profile_or_username.username:
|
| 57 |
+
return profile_or_username.username
|
| 58 |
+
# Check if profile_or_username is a direct username string
|
| 59 |
+
elif isinstance(profile_or_username, str) and profile_or_username.strip():
|
| 60 |
+
# Extract username from "Logout (username)" format
|
| 61 |
+
if profile_or_username.startswith("Logout (") and profile_or_username.endswith(")"):
|
| 62 |
+
return profile_or_username[8:-1] # Remove "Logout (" and ")"
|
| 63 |
+
# If it's just a username string, return it
|
| 64 |
+
elif profile_or_username != "Sign in with Hugging Face":
|
| 65 |
+
return profile_or_username.strip()
|
| 66 |
+
|
| 67 |
+
# Return "Anonymous" if no valid username found
|
| 68 |
+
return "Anonymous"
|
| 69 |
+
|
| 70 |
+
def process_image(image):
|
| 71 |
+
"""Process uploaded image and select random models for comparison."""
|
| 72 |
+
global current_gemini_output, current_mistral_output, current_openai_output, current_image_url, current_voted_users, current_model_a, current_model_b
|
| 73 |
+
|
| 74 |
+
if image is None:
|
| 75 |
+
return (
|
| 76 |
+
"Please upload an image.",
|
| 77 |
+
"Please upload an image.",
|
| 78 |
+
gr.update(visible=False), # Hide vote buttons
|
| 79 |
+
gr.update(visible=False) # Hide vote buttons
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# Reset voted users for new image
|
| 83 |
+
current_voted_users.clear()
|
| 84 |
+
|
| 85 |
+
# Select two random models
|
| 86 |
+
model_a, model_b = select_random_models()
|
| 87 |
+
current_model_a = model_a
|
| 88 |
+
current_model_b = model_b
|
| 89 |
+
|
| 90 |
+
logger.info(f"🎲 Randomly selected two models for comparison")
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
# Save the PIL image to a temporary file
|
| 94 |
+
temp_filename = f"temp_image_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
| 95 |
+
image.save(temp_filename)
|
| 96 |
+
|
| 97 |
+
# Upload the temporary file to Supabase storage
|
| 98 |
+
logger.info(f"📤 Uploading image to Supabase storage: {temp_filename}")
|
| 99 |
+
upload_result = upload_file_to_bucket(
|
| 100 |
+
file_path=temp_filename,
|
| 101 |
+
bucket_name="images",
|
| 102 |
+
storage_path=f"ocr_images/{temp_filename}",
|
| 103 |
+
file_options={"cache-control": "3600", "upsert": "false"}
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
if upload_result["success"]:
|
| 107 |
+
logger.info(f"✅ Image uploaded successfully: {upload_result['storage_path']}")
|
| 108 |
+
logger.info(f"🔗 Public URL: {upload_result['public_url']}")
|
| 109 |
+
# Store the image URL for voting
|
| 110 |
+
current_image_url = upload_result.get('public_url') or f"{SUPABASE_URL}/storage/v1/object/public/images/ocr_images/{temp_filename}"
|
| 111 |
+
else:
|
| 112 |
+
logger.error(f"❌ Image upload failed: {upload_result['error']}")
|
| 113 |
+
current_image_url = ""
|
| 114 |
+
|
| 115 |
+
# Clean up temporary file
|
| 116 |
+
try:
|
| 117 |
+
os.remove(temp_filename)
|
| 118 |
+
logger.info(f"🗑️ Cleaned up temporary file: {temp_filename}")
|
| 119 |
+
except Exception as e:
|
| 120 |
+
logger.warning(f"⚠️ Could not remove temporary file {temp_filename}: {e}")
|
| 121 |
+
|
| 122 |
+
# Return initial state - OCR processing will happen via separate button clicks
|
| 123 |
+
return (
|
| 124 |
+
"Please click 'Run OCR' to start processing.",
|
| 125 |
+
"Please click 'Run OCR' to start processing.",
|
| 126 |
+
gr.update(visible=False), # Hide vote buttons initially
|
| 127 |
+
gr.update(visible=False) # Hide vote buttons initially
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
except Exception as e:
|
| 131 |
+
logger.error(f"Error processing image: {e}")
|
| 132 |
+
return (
|
| 133 |
+
f"Error processing image: {e}",
|
| 134 |
+
f"Error processing image: {e}",
|
| 135 |
+
gr.update(visible=False), # Hide vote buttons
|
| 136 |
+
gr.update(visible=False) # Hide vote buttons
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
def check_ocr_completion(model_a_output, model_b_output):
|
| 140 |
+
"""Check if both OCR results are ready and update UI accordingly."""
|
| 141 |
+
global current_gemini_output, current_mistral_output, current_openai_output, current_gpt5_output, current_model_a, current_model_b
|
| 142 |
+
|
| 143 |
+
# Check if both results are complete (not processing messages)
|
| 144 |
+
model_a_ready = (model_a_output and
|
| 145 |
+
model_a_output != "Please upload an image." and
|
| 146 |
+
model_a_output != "Processing OCR..." and
|
| 147 |
+
model_a_output != "Please click 'Run OCR' to start processing." and
|
| 148 |
+
not model_a_output.startswith("OCR error:"))
|
| 149 |
+
|
| 150 |
+
model_b_ready = (model_b_output and
|
| 151 |
+
model_b_output != "Please upload an image." and
|
| 152 |
+
model_b_output != "Processing OCR..." and
|
| 153 |
+
model_b_output != "Please click 'Run OCR' to start processing." and
|
| 154 |
+
not model_b_output.startswith("OCR error:"))
|
| 155 |
+
|
| 156 |
+
# Update global variables with actual results based on which models were selected
|
| 157 |
+
if model_a_ready:
|
| 158 |
+
if current_model_a == "gemini":
|
| 159 |
+
current_gemini_output = model_a_output
|
| 160 |
+
elif current_model_a == "mistral":
|
| 161 |
+
current_mistral_output = model_a_output
|
| 162 |
+
elif current_model_a == "openai":
|
| 163 |
+
current_openai_output = model_a_output
|
| 164 |
+
elif current_model_a == "gpt5":
|
| 165 |
+
current_gpt5_output = model_a_output
|
| 166 |
+
|
| 167 |
+
if model_b_ready:
|
| 168 |
+
if current_model_b == "gemini":
|
| 169 |
+
current_gemini_output = model_b_output
|
| 170 |
+
elif current_model_b == "mistral":
|
| 171 |
+
current_mistral_output = model_b_output
|
| 172 |
+
elif current_model_b == "openai":
|
| 173 |
+
current_openai_output = model_b_output
|
| 174 |
+
elif current_model_b == "gpt5":
|
| 175 |
+
current_gpt5_output = model_b_output
|
| 176 |
+
|
| 177 |
+
# Show vote buttons only when both are ready
|
| 178 |
+
if model_a_ready and model_b_ready:
|
| 179 |
+
return (
|
| 180 |
+
gr.update(visible=True), # Show Model A vote button
|
| 181 |
+
gr.update(visible=True) # Show Model B vote button
|
| 182 |
+
)
|
| 183 |
+
else:
|
| 184 |
+
return (
|
| 185 |
+
gr.update(visible=False), # Hide vote buttons
|
| 186 |
+
gr.update(visible=False) # Hide vote buttons
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
def load_vote_data():
|
| 190 |
+
"""Load and format vote data for display."""
|
| 191 |
+
try:
|
| 192 |
+
# Get all votes
|
| 193 |
+
votes = get_all_votes()
|
| 194 |
+
votes_table_html = format_votes_table(votes)
|
| 195 |
+
|
| 196 |
+
return votes_table_html
|
| 197 |
+
|
| 198 |
+
except Exception as e:
|
| 199 |
+
logger.error(f"Error loading vote data: {e}")
|
| 200 |
+
error_html = f"<p style='color: red;'>Error loading data: {e}</p>"
|
| 201 |
+
return error_html
|
| 202 |
+
|
| 203 |
+
def load_elo_leaderboard():
|
| 204 |
+
"""Load and format ELO leaderboard data."""
|
| 205 |
+
try:
|
| 206 |
+
# Get all votes
|
| 207 |
+
votes = get_all_votes()
|
| 208 |
+
|
| 209 |
+
# Calculate ELO ratings
|
| 210 |
+
elo_ratings = calculate_elo_ratings_from_votes(votes)
|
| 211 |
+
|
| 212 |
+
# Calculate vote counts for each model
|
| 213 |
+
vote_counts = {
|
| 214 |
+
"gemini": 0,
|
| 215 |
+
"mistral": 0,
|
| 216 |
+
"openai": 0,
|
| 217 |
+
"gpt5": 0
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
for vote in votes:
|
| 221 |
+
model_a = vote.get('model_a')
|
| 222 |
+
model_b = vote.get('model_b')
|
| 223 |
+
vote_choice = vote.get('vote')
|
| 224 |
+
|
| 225 |
+
if vote_choice == 'model_a' and model_a in vote_counts:
|
| 226 |
+
vote_counts[model_a] += 1
|
| 227 |
+
elif vote_choice == 'model_b' and model_b in vote_counts:
|
| 228 |
+
vote_counts[model_b] += 1
|
| 229 |
+
|
| 230 |
+
# Format leaderboard with vote counts
|
| 231 |
+
leaderboard_html = format_elo_leaderboard(elo_ratings, vote_counts)
|
| 232 |
+
|
| 233 |
+
return leaderboard_html
|
| 234 |
+
|
| 235 |
+
except Exception as e:
|
| 236 |
+
logger.error(f"Error loading ELO leaderboard: {e}")
|
| 237 |
+
error_html = f"<p style='color: red;'>Error loading ELO leaderboard: {e}</p>"
|
| 238 |
+
return error_html
|
| 239 |
+
|
| 240 |
+
# Create the Gradio interface
|
| 241 |
+
with gr.Blocks(title="OCR Comparison", css="""
|
| 242 |
+
.output-box {
|
| 243 |
+
border: 2px solid #e0e0e0;
|
| 244 |
+
border-radius: 8px;
|
| 245 |
+
padding: 15px;
|
| 246 |
+
margin: 10px 0;
|
| 247 |
+
background-color: #f9f9f9;
|
| 248 |
+
min-height: 200px;
|
| 249 |
+
}
|
| 250 |
+
.output-box:hover {
|
| 251 |
+
border-color: #007bff;
|
| 252 |
+
box-shadow: 0 2px 8px rgba(0,123,255,0.1);
|
| 253 |
+
}
|
| 254 |
+
.vote-table {
|
| 255 |
+
border-collapse: collapse;
|
| 256 |
+
width: 100%;
|
| 257 |
+
margin: 10px 0;
|
| 258 |
+
min-width: 800px;
|
| 259 |
+
}
|
| 260 |
+
.vote-table th, .vote-table td {
|
| 261 |
+
border: 1px solid #ddd;
|
| 262 |
+
padding: 6px;
|
| 263 |
+
text-align: left;
|
| 264 |
+
vertical-align: top;
|
| 265 |
+
}
|
| 266 |
+
.vote-table th {
|
| 267 |
+
background-color: #f2f2f2;
|
| 268 |
+
font-weight: bold;
|
| 269 |
+
position: sticky;
|
| 270 |
+
top: 0;
|
| 271 |
+
z-index: 10;
|
| 272 |
+
}
|
| 273 |
+
.vote-table tr:nth-child(even) {
|
| 274 |
+
background-color: #f9f9f9;
|
| 275 |
+
}
|
| 276 |
+
.vote-table tr:hover {
|
| 277 |
+
background-color: #f5f5f5;
|
| 278 |
+
}
|
| 279 |
+
.vote-table img {
|
| 280 |
+
transition: transform 0.2s ease;
|
| 281 |
+
max-width: 100%;
|
| 282 |
+
height: auto;
|
| 283 |
+
}
|
| 284 |
+
.vote-table img:hover {
|
| 285 |
+
transform: scale(1.1);
|
| 286 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
| 287 |
+
}
|
| 288 |
+
""") as demo:
|
| 289 |
+
|
| 290 |
+
with gr.Tabs():
|
| 291 |
+
# Arena Tab (default)
|
| 292 |
+
with gr.Tab("⚔️ Arena", id=0):
|
| 293 |
+
gr.Markdown("# ⚔️ OCR Arena: Random Model Selection")
|
| 294 |
+
gr.Markdown("Upload an image to compare two randomly selected OCR models.")
|
| 295 |
+
|
| 296 |
+
# Authentication section (optional)
|
| 297 |
+
with gr.Row():
|
| 298 |
+
with gr.Column(scale=3):
|
| 299 |
+
username_display = gr.Textbox(
|
| 300 |
+
label="Current User",
|
| 301 |
+
placeholder="Login with Hugging Face to vote (optional) - Anonymous users welcome!",
|
| 302 |
+
interactive=False,
|
| 303 |
+
show_label=False
|
| 304 |
+
)
|
| 305 |
+
with gr.Column(scale=1):
|
| 306 |
+
login_button = gr.LoginButton()
|
| 307 |
+
|
| 308 |
+
with gr.Row():
|
| 309 |
+
with gr.Column():
|
| 310 |
+
gemini_vote_btn = gr.Button("A is better", variant="primary", size="sm", visible=False)
|
| 311 |
+
gemini_output = gr.Markdown(label="Model A Output", elem_classes=["output-box"])
|
| 312 |
+
|
| 313 |
+
image_input = gr.Image(type="pil", label="Upload or Paste Image")
|
| 314 |
+
|
| 315 |
+
with gr.Column():
|
| 316 |
+
mistral_vote_btn = gr.Button("B is better", variant="primary", size="sm", visible=False)
|
| 317 |
+
mistral_output = gr.Markdown(label="Model B Output", elem_classes=["output-box"])
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
with gr.Row():
|
| 322 |
+
process_btn = gr.Button("🔍 Run OCR", variant="primary")
|
| 323 |
+
|
| 324 |
+
# Data Tab
|
| 325 |
+
with gr.Tab("📊 Data", id=1):
|
| 326 |
+
gr.Markdown("# 📊 Vote Data")
|
| 327 |
+
gr.Markdown("View all votes from the OCR Arena")
|
| 328 |
+
|
| 329 |
+
with gr.Row():
|
| 330 |
+
refresh_btn = gr.Button("🔄 Refresh Data", variant="secondary")
|
| 331 |
+
|
| 332 |
+
with gr.Row():
|
| 333 |
+
votes_table = gr.HTML(
|
| 334 |
+
value="<p>Loading vote data...</p>",
|
| 335 |
+
label="📋 All Votes (Latest First)"
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# Leaderboard Tab
|
| 339 |
+
with gr.Tab("🏆 Leaderboard", id=2):
|
| 340 |
+
gr.Markdown("# 🏆 ELO Leaderboard")
|
| 341 |
+
gr.Markdown("See how the models rank based on their ELO ratings from head-to-head comparisons.")
|
| 342 |
+
|
| 343 |
+
with gr.Row():
|
| 344 |
+
refresh_leaderboard_btn = gr.Button("🔄 Refresh Leaderboard", variant="secondary")
|
| 345 |
+
|
| 346 |
+
with gr.Row():
|
| 347 |
+
leaderboard_display = gr.HTML(
|
| 348 |
+
value="<p>Loading ELO leaderboard...</p>",
|
| 349 |
+
label="🏆 Model Rankings"
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
# Vote functions
|
| 353 |
+
def vote_model_a(profile_or_username):
|
| 354 |
+
global current_gemini_output, current_mistral_output, current_openai_output, current_gpt5_output, current_image_url, current_voted_users, current_model_a, current_model_b
|
| 355 |
+
|
| 356 |
+
# Get current username
|
| 357 |
+
username = get_current_username(profile_or_username)
|
| 358 |
+
|
| 359 |
+
if not username:
|
| 360 |
+
username = "Anonymous"
|
| 361 |
+
|
| 362 |
+
# Check if user has already voted
|
| 363 |
+
if username in current_voted_users:
|
| 364 |
+
gr.Info(f"You have already voted for this image, {username}!")
|
| 365 |
+
return
|
| 366 |
+
|
| 367 |
+
try:
|
| 368 |
+
# Use the stored image URL from the upload
|
| 369 |
+
image_url = current_image_url if current_image_url else "no_image"
|
| 370 |
+
|
| 371 |
+
# Add vote to database
|
| 372 |
+
logger.info(f"📊 Adding Model A vote for user: {username}")
|
| 373 |
+
def output_for(model: str) -> str:
|
| 374 |
+
return {
|
| 375 |
+
"gemini": current_gemini_output,
|
| 376 |
+
"mistral": current_mistral_output,
|
| 377 |
+
"openai": current_openai_output,
|
| 378 |
+
"gpt5": current_gpt5_output,
|
| 379 |
+
}.get(model, "")
|
| 380 |
+
|
| 381 |
+
add_vote(
|
| 382 |
+
username=username,
|
| 383 |
+
model_a=current_model_a,
|
| 384 |
+
model_b=current_model_b,
|
| 385 |
+
model_a_output=output_for(current_model_a),
|
| 386 |
+
model_b_output=output_for(current_model_b),
|
| 387 |
+
vote="model_a",
|
| 388 |
+
image_url=image_url
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
# Mark user as voted
|
| 392 |
+
current_voted_users.add(username)
|
| 393 |
+
|
| 394 |
+
model_a_name = get_model_display_name(current_model_a)
|
| 395 |
+
model_b_name = get_model_display_name(current_model_b)
|
| 396 |
+
info_message = (
|
| 397 |
+
f"<p>You voted for <strong style='color:green;'>{model_a_name}</strong>.</p>"
|
| 398 |
+
f"<p><span style='color:green;'>{model_a_name}</span> - "
|
| 399 |
+
f"<span style='color:blue;'>{model_b_name}</span></p>"
|
| 400 |
+
)
|
| 401 |
+
gr.Info(info_message)
|
| 402 |
+
|
| 403 |
+
except Exception as e:
|
| 404 |
+
logger.error(f"❌ Error adding Model A vote: {e}")
|
| 405 |
+
gr.Info(f"Error recording vote: {e}")
|
| 406 |
+
|
| 407 |
+
def vote_model_b(profile_or_username):
|
| 408 |
+
global current_gemini_output, current_mistral_output, current_openai_output, current_gpt5_output, current_image_url, current_voted_users, current_model_a, current_model_b
|
| 409 |
+
|
| 410 |
+
# Get current username
|
| 411 |
+
username = get_current_username(profile_or_username)
|
| 412 |
+
|
| 413 |
+
if not username:
|
| 414 |
+
username = "Anonymous"
|
| 415 |
+
|
| 416 |
+
# Check if user has already voted
|
| 417 |
+
if username in current_voted_users:
|
| 418 |
+
gr.Info(f"You have already voted for this image, {username}!")
|
| 419 |
+
return
|
| 420 |
+
|
| 421 |
+
try:
|
| 422 |
+
# Use the stored image URL from the upload
|
| 423 |
+
image_url = current_image_url if current_image_url else "no_image"
|
| 424 |
+
|
| 425 |
+
# Add vote to database
|
| 426 |
+
logger.info(f"📊 Adding Model B vote for user: {username}")
|
| 427 |
+
def output_for(model: str) -> str:
|
| 428 |
+
return {
|
| 429 |
+
"gemini": current_gemini_output,
|
| 430 |
+
"mistral": current_mistral_output,
|
| 431 |
+
"openai": current_openai_output,
|
| 432 |
+
"gpt5": current_gpt5_output,
|
| 433 |
+
}.get(model, "")
|
| 434 |
+
|
| 435 |
+
add_vote(
|
| 436 |
+
username=username,
|
| 437 |
+
model_a=current_model_a,
|
| 438 |
+
model_b=current_model_b,
|
| 439 |
+
model_a_output=output_for(current_model_a),
|
| 440 |
+
model_b_output=output_for(current_model_b),
|
| 441 |
+
vote="model_b",
|
| 442 |
+
image_url=image_url
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
# Mark user as voted
|
| 446 |
+
current_voted_users.add(username)
|
| 447 |
+
|
| 448 |
+
model_a_name = get_model_display_name(current_model_a)
|
| 449 |
+
model_b_name = get_model_display_name(current_model_b)
|
| 450 |
+
info_message = (
|
| 451 |
+
f"<p>You voted for <strong style='color:blue;'>{model_b_name}</strong>.</p>"
|
| 452 |
+
f"<p><span style='color:green;'>{model_a_name}</span> - "
|
| 453 |
+
f"<span style='color:blue;'>{model_b_name}</span></p>"
|
| 454 |
+
)
|
| 455 |
+
gr.Info(info_message)
|
| 456 |
+
|
| 457 |
+
except Exception as e:
|
| 458 |
+
logger.error(f"❌ Error adding Model B vote: {e}")
|
| 459 |
+
gr.Info(f"Error recording vote: {e}")
|
| 460 |
+
|
| 461 |
+
# Event handlers
|
| 462 |
+
process_btn.click(
|
| 463 |
+
process_image,
|
| 464 |
+
inputs=[image_input],
|
| 465 |
+
outputs=[gemini_output, mistral_output, gemini_vote_btn, mistral_vote_btn],
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
# Process both randomly selected OCRs when the process button is clicked
|
| 469 |
+
def process_model_a_ocr(image):
|
| 470 |
+
global current_model_a
|
| 471 |
+
return process_model_ocr(image, current_model_a)
|
| 472 |
+
|
| 473 |
+
def process_model_b_ocr(image):
|
| 474 |
+
global current_model_b
|
| 475 |
+
return process_model_ocr(image, current_model_b)
|
| 476 |
+
|
| 477 |
+
process_btn.click(
|
| 478 |
+
process_model_a_ocr,
|
| 479 |
+
inputs=[image_input],
|
| 480 |
+
outputs=[gemini_output],
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
process_btn.click(
|
| 484 |
+
process_model_b_ocr,
|
| 485 |
+
inputs=[image_input],
|
| 486 |
+
outputs=[mistral_output],
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
# Check completion status when either OCR output changes
|
| 490 |
+
gemini_output.change(
|
| 491 |
+
check_ocr_completion,
|
| 492 |
+
inputs=[gemini_output, mistral_output],
|
| 493 |
+
outputs=[gemini_vote_btn, mistral_vote_btn],
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
mistral_output.change(
|
| 497 |
+
check_ocr_completion,
|
| 498 |
+
inputs=[gemini_output, mistral_output],
|
| 499 |
+
outputs=[gemini_vote_btn, mistral_vote_btn],
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
gemini_vote_btn.click(
|
| 503 |
+
vote_model_a,
|
| 504 |
+
inputs=[login_button]
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
mistral_vote_btn.click(
|
| 508 |
+
vote_model_b,
|
| 509 |
+
inputs=[login_button]
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
# Refresh data button
|
| 513 |
+
refresh_btn.click(
|
| 514 |
+
load_vote_data,
|
| 515 |
+
inputs=None,
|
| 516 |
+
outputs=[votes_table]
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
# Refresh leaderboard button
|
| 520 |
+
refresh_leaderboard_btn.click(
|
| 521 |
+
load_elo_leaderboard,
|
| 522 |
+
inputs=None,
|
| 523 |
+
outputs=[leaderboard_display]
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
# Update username display when user logs in
|
| 527 |
+
demo.load(fn=get_default_username, inputs=None, outputs=username_display)
|
| 528 |
+
|
| 529 |
+
# Load vote data when app starts
|
| 530 |
+
demo.load(fn=load_vote_data, inputs=None, outputs=[votes_table])
|
| 531 |
+
|
| 532 |
+
# Load leaderboard when app starts
|
| 533 |
+
demo.load(fn=load_elo_leaderboard, inputs=None, outputs=[leaderboard_display])
|
| 534 |
+
|
| 535 |
+
if __name__ == "__main__":
|
| 536 |
+
logger.info("Starting OCR Comparison App...")
|
| 537 |
+
try:
|
| 538 |
+
# Try to launch on localhost first
|
| 539 |
+
demo.launch(share=True)
|
| 540 |
+
except ValueError as e:
|
| 541 |
+
logger.warning(f"Localhost not accessible: {e}")
|
| 542 |
+
logger.info("Launching with public URL...")
|
| 543 |
+
demo.launch(share=True)
|
db.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HTTP-based Supabase connector for OCR Arena votes.
|
| 2 |
+
|
| 3 |
+
This module provides a connection to Supabase using HTTP requests,
|
| 4 |
+
avoiding the dependency issues with the supabase client library.
|
| 5 |
+
"""
|
| 6 |
+
import logging
|
| 7 |
+
import requests
|
| 8 |
+
import json
|
| 9 |
+
import math
|
| 10 |
+
from typing import Dict, Any, List
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
| 17 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Supabase API configuration
|
| 23 |
+
API_BASE_URL = f"{SUPABASE_URL}/rest/v1"
|
| 24 |
+
HEADERS = {
|
| 25 |
+
"apikey": SUPABASE_KEY,
|
| 26 |
+
"Authorization": f"Bearer {SUPABASE_KEY}",
|
| 27 |
+
"Content-Type": "application/json"
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
def test_connection() -> bool:
|
| 31 |
+
"""Test the Supabase connection."""
|
| 32 |
+
try:
|
| 33 |
+
# Test connection by trying to access the ocr_votes table
|
| 34 |
+
table_url = f"{API_BASE_URL}/ocr_votes"
|
| 35 |
+
response = requests.get(table_url, headers=HEADERS)
|
| 36 |
+
if response.status_code in [200, 404]: # 200 = table exists, 404 = table doesn't exist but connection works
|
| 37 |
+
logger.info("✅ Supabase connection test successful")
|
| 38 |
+
return True
|
| 39 |
+
else:
|
| 40 |
+
logger.error(f"❌ Supabase connection failed: {response.status_code}")
|
| 41 |
+
return False
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.error(f"❌ Supabase connection test failed: {e}")
|
| 44 |
+
return False
|
| 45 |
+
|
| 46 |
+
def test_table_exists(table_name: str = "ocr_votes") -> bool:
|
| 47 |
+
"""Test if a specific table exists in the database."""
|
| 48 |
+
try:
|
| 49 |
+
table_url = f"{API_BASE_URL}/{table_name}"
|
| 50 |
+
response = requests.get(table_url, headers=HEADERS)
|
| 51 |
+
if response.status_code == 200:
|
| 52 |
+
logger.info(f"✅ Table '{table_name}' exists and is accessible")
|
| 53 |
+
return True
|
| 54 |
+
else:
|
| 55 |
+
logger.warning(f"⚠️ Table '{table_name}' may not exist: {response.status_code}")
|
| 56 |
+
return False
|
| 57 |
+
except Exception as e:
|
| 58 |
+
logger.error(f"❌ Error testing table access: {e}")
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
def add_vote(
|
| 62 |
+
username: str,
|
| 63 |
+
model_a: str,
|
| 64 |
+
model_b: str,
|
| 65 |
+
model_a_output: str,
|
| 66 |
+
model_b_output: str,
|
| 67 |
+
vote: str,
|
| 68 |
+
image_url: str
|
| 69 |
+
) -> Dict[str, Any]:
|
| 70 |
+
"""Add a vote to the ocr_votes table."""
|
| 71 |
+
try:
|
| 72 |
+
# Format timestamp in the desired format: YYYY-MM-DD HH:MM:SS
|
| 73 |
+
from datetime import datetime
|
| 74 |
+
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 75 |
+
|
| 76 |
+
data = {
|
| 77 |
+
"username": username,
|
| 78 |
+
"model_a": model_a,
|
| 79 |
+
"model_b": model_b,
|
| 80 |
+
"model_a_output": model_a_output,
|
| 81 |
+
"model_b_output": model_b_output,
|
| 82 |
+
"vote": vote,
|
| 83 |
+
"image_url": image_url,
|
| 84 |
+
"timestamp": timestamp
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
table_url = f"{API_BASE_URL}/ocr_votes"
|
| 88 |
+
response = requests.post(table_url, headers=HEADERS, json=data)
|
| 89 |
+
|
| 90 |
+
if response.status_code == 201:
|
| 91 |
+
logger.info("✅ Vote added successfully")
|
| 92 |
+
try:
|
| 93 |
+
return response.json()[0] if response.json() else data
|
| 94 |
+
except json.JSONDecodeError:
|
| 95 |
+
return data
|
| 96 |
+
else:
|
| 97 |
+
raise Exception(f"Insert failed with status {response.status_code}: {response.text}")
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
logger.error(f"❌ Error adding vote: {e}")
|
| 101 |
+
raise
|
| 102 |
+
|
| 103 |
+
def get_all_votes() -> List[Dict[str, Any]]:
|
| 104 |
+
"""Get all votes from the ocr_votes table."""
|
| 105 |
+
try:
|
| 106 |
+
table_url = f"{API_BASE_URL}/ocr_votes"
|
| 107 |
+
response = requests.get(table_url, headers=HEADERS)
|
| 108 |
+
|
| 109 |
+
if response.status_code == 200:
|
| 110 |
+
try:
|
| 111 |
+
return response.json()
|
| 112 |
+
except json.JSONDecodeError:
|
| 113 |
+
logger.warning("Could not parse JSON response")
|
| 114 |
+
return []
|
| 115 |
+
else:
|
| 116 |
+
logger.error(f"Failed to get votes: {response.status_code}")
|
| 117 |
+
return []
|
| 118 |
+
except Exception as e:
|
| 119 |
+
logger.error(f"❌ Error getting votes: {e}")
|
| 120 |
+
return []
|
| 121 |
+
|
| 122 |
+
def test_add_sample_vote() -> bool:
|
| 123 |
+
"""Test adding a sample vote to the database."""
|
| 124 |
+
try:
|
| 125 |
+
sample_vote = add_vote(
|
| 126 |
+
username="test_user",
|
| 127 |
+
model_a="gemini",
|
| 128 |
+
model_b="mistral",
|
| 129 |
+
model_a_output="# Test Gemini Output\n\nThis is a **test** markdown from Gemini.",
|
| 130 |
+
model_b_output="## Test Mistral Output\n\nThis is a *test* markdown from Mistral.",
|
| 131 |
+
vote="model_a",
|
| 132 |
+
image_url="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
|
| 133 |
+
)
|
| 134 |
+
logger.info(f"✅ Sample vote added: {sample_vote}")
|
| 135 |
+
return True
|
| 136 |
+
except Exception as e:
|
| 137 |
+
logger.error(f"❌ Error adding sample vote: {e}")
|
| 138 |
+
return False
|
| 139 |
+
|
| 140 |
+
def get_vote_statistics() -> Dict[str, Any]:
|
| 141 |
+
"""Get voting statistics."""
|
| 142 |
+
try:
|
| 143 |
+
votes = get_all_votes()
|
| 144 |
+
|
| 145 |
+
# Count votes for each model
|
| 146 |
+
gemini_votes = 0
|
| 147 |
+
mistral_votes = 0
|
| 148 |
+
openai_votes = 0
|
| 149 |
+
gpt5_votes = 0
|
| 150 |
+
total_votes = len(votes)
|
| 151 |
+
|
| 152 |
+
for vote in votes:
|
| 153 |
+
vote_choice = vote.get('vote')
|
| 154 |
+
model_a = vote.get('model_a')
|
| 155 |
+
model_b = vote.get('model_b')
|
| 156 |
+
|
| 157 |
+
if vote_choice == 'model_a':
|
| 158 |
+
if model_a == 'gemini':
|
| 159 |
+
gemini_votes += 1
|
| 160 |
+
elif model_a == 'mistral':
|
| 161 |
+
mistral_votes += 1
|
| 162 |
+
elif model_a == 'openai':
|
| 163 |
+
openai_votes += 1
|
| 164 |
+
elif model_a == 'gpt5':
|
| 165 |
+
gpt5_votes += 1
|
| 166 |
+
elif vote_choice == 'model_b':
|
| 167 |
+
if model_b == 'gemini':
|
| 168 |
+
gemini_votes += 1
|
| 169 |
+
elif model_b == 'mistral':
|
| 170 |
+
mistral_votes += 1
|
| 171 |
+
elif model_b == 'openai':
|
| 172 |
+
openai_votes += 1
|
| 173 |
+
elif model_b == 'gpt5':
|
| 174 |
+
gpt5_votes += 1
|
| 175 |
+
|
| 176 |
+
return {
|
| 177 |
+
"total_votes": total_votes,
|
| 178 |
+
"gemini_votes": gemini_votes,
|
| 179 |
+
"mistral_votes": mistral_votes,
|
| 180 |
+
"openai_votes": openai_votes,
|
| 181 |
+
"gpt5_votes": gpt5_votes,
|
| 182 |
+
"gemini_percentage": (gemini_votes / total_votes * 100) if total_votes > 0 else 0,
|
| 183 |
+
"mistral_percentage": (mistral_votes / total_votes * 100) if total_votes > 0 else 0,
|
| 184 |
+
"openai_percentage": (openai_votes / total_votes * 100) if total_votes > 0 else 0,
|
| 185 |
+
"gpt5_percentage": (gpt5_votes / total_votes * 100) if total_votes > 0 else 0
|
| 186 |
+
}
|
| 187 |
+
except Exception as e:
|
| 188 |
+
logger.error(f"❌ Error getting vote statistics: {e}")
|
| 189 |
+
return {}
|
| 190 |
+
|
| 191 |
+
def calculate_elo_rating(rating_a: float, rating_b: float, result_a: float, k_factor: int = 32) -> tuple[float, float]:
|
| 192 |
+
"""
|
| 193 |
+
Calculate new ELO ratings for two players after a match.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
rating_a: Current ELO rating of player A
|
| 197 |
+
rating_b: Current ELO rating of player B
|
| 198 |
+
result_a: Result for player A (1 for win, 0.5 for draw, 0 for loss)
|
| 199 |
+
k_factor: K-factor determines how much a single result affects the rating
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
tuple: (new_rating_a, new_rating_b)
|
| 203 |
+
"""
|
| 204 |
+
# Calculate expected scores
|
| 205 |
+
expected_a = 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
|
| 206 |
+
expected_b = 1 / (1 + 10 ** ((rating_a - rating_b) / 400))
|
| 207 |
+
|
| 208 |
+
# Calculate new ratings
|
| 209 |
+
new_rating_a = rating_a + k_factor * (result_a - expected_a)
|
| 210 |
+
new_rating_b = rating_b + k_factor * ((1 - result_a) - expected_b)
|
| 211 |
+
|
| 212 |
+
return new_rating_a, new_rating_b
|
| 213 |
+
|
| 214 |
+
def calculate_elo_ratings_from_votes(votes: List[Dict[str, Any]]) -> Dict[str, float]:
|
| 215 |
+
"""
|
| 216 |
+
Calculate ELO ratings for all models based on vote history.
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
votes: List of vote dictionaries from database
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
dict: Current ELO ratings for each model
|
| 223 |
+
"""
|
| 224 |
+
# Initialize ELO ratings (starting at 1500)
|
| 225 |
+
elo_ratings = {
|
| 226 |
+
"gemini": 1500,
|
| 227 |
+
"mistral": 1500,
|
| 228 |
+
"openai": 1500,
|
| 229 |
+
"gpt5": 1500
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
# Process each vote to update ELO ratings
|
| 233 |
+
for vote in votes:
|
| 234 |
+
model_a = vote.get('model_a')
|
| 235 |
+
model_b = vote.get('model_b')
|
| 236 |
+
vote_choice = vote.get('vote')
|
| 237 |
+
|
| 238 |
+
if model_a and model_b and vote_choice:
|
| 239 |
+
# Determine result for model A
|
| 240 |
+
if vote_choice == 'model_a':
|
| 241 |
+
result_a = 1 # Model A wins
|
| 242 |
+
elif vote_choice == 'model_b':
|
| 243 |
+
result_a = 0 # Model A loses
|
| 244 |
+
else:
|
| 245 |
+
continue # Skip invalid votes
|
| 246 |
+
|
| 247 |
+
# Calculate new ELO ratings
|
| 248 |
+
new_rating_a, new_rating_b = calculate_elo_rating(
|
| 249 |
+
elo_ratings[model_a],
|
| 250 |
+
elo_ratings[model_b],
|
| 251 |
+
result_a
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# Update ratings
|
| 255 |
+
elo_ratings[model_a] = new_rating_a
|
| 256 |
+
elo_ratings[model_b] = new_rating_b
|
| 257 |
+
|
| 258 |
+
return elo_ratings
|
| 259 |
+
|
| 260 |
+
if __name__ == "__main__":
|
| 261 |
+
print(test_connection())
|
| 262 |
+
print(test_add_sample_vote())
|
ocr_models.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OCR Models Module
|
| 3 |
+
Contains all OCR-related functions for different AI models.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import google.generativeai as genai
|
| 7 |
+
from mistralai import Mistral
|
| 8 |
+
from PIL import Image
|
| 9 |
+
import io
|
| 10 |
+
import base64
|
| 11 |
+
import logging
|
| 12 |
+
import openai
|
| 13 |
+
import os
|
| 14 |
+
|
| 15 |
+
# Configure logging
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
def gemini_ocr(image: Image.Image):
|
| 19 |
+
"""Process OCR using Google's Gemini 2.0 Flash model."""
|
| 20 |
+
try:
|
| 21 |
+
# Initialize Gemini model
|
| 22 |
+
gemini_model = initialize_gemini()
|
| 23 |
+
if not gemini_model:
|
| 24 |
+
return "Gemini OCR error: Failed to initialize Gemini model"
|
| 25 |
+
|
| 26 |
+
# Convert image to base64
|
| 27 |
+
buffered = io.BytesIO()
|
| 28 |
+
image.save(buffered, format="JPEG")
|
| 29 |
+
img_bytes = buffered.getvalue()
|
| 30 |
+
base64_image = base64.b64encode(img_bytes).decode('utf-8')
|
| 31 |
+
|
| 32 |
+
# Create the image part for Gemini
|
| 33 |
+
image_part = {
|
| 34 |
+
"mime_type": "image/jpeg",
|
| 35 |
+
"data": base64_image
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# Generate content with Gemini
|
| 39 |
+
response = gemini_model.generate_content([
|
| 40 |
+
"Extract and transcribe all text from this image. Return only the transcribed text in markdown format, preserving any formatting like headers, lists, etc.",
|
| 41 |
+
image_part
|
| 42 |
+
])
|
| 43 |
+
|
| 44 |
+
markdown_text = response.text
|
| 45 |
+
logger.info("Gemini OCR completed successfully")
|
| 46 |
+
return markdown_text
|
| 47 |
+
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logger.error(f"Gemini OCR error: {e}")
|
| 50 |
+
return f"Gemini OCR error: {e}"
|
| 51 |
+
|
| 52 |
+
def mistral_ocr(image: Image.Image):
|
| 53 |
+
"""Process OCR using Mistral AI's OCR model."""
|
| 54 |
+
try:
|
| 55 |
+
# Convert image to base64
|
| 56 |
+
buffered = io.BytesIO()
|
| 57 |
+
image.save(buffered, format="JPEG")
|
| 58 |
+
img_bytes = buffered.getvalue()
|
| 59 |
+
base64_image = base64.b64encode(img_bytes).decode('utf-8')
|
| 60 |
+
|
| 61 |
+
client = Mistral(api_key=os.getenv("MISTRAL_API_KEY"))
|
| 62 |
+
ocr_response = client.ocr.process(
|
| 63 |
+
model="mistral-ocr-latest",
|
| 64 |
+
document={
|
| 65 |
+
"type": "image_url",
|
| 66 |
+
"image_url": f"data:image/jpeg;base64,{base64_image}"
|
| 67 |
+
}
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Extract markdown from the first page if available
|
| 71 |
+
markdown_text = ""
|
| 72 |
+
if hasattr(ocr_response, 'pages') and ocr_response.pages:
|
| 73 |
+
page = ocr_response.pages[0]
|
| 74 |
+
markdown_text = getattr(page, 'markdown', "")
|
| 75 |
+
|
| 76 |
+
if not markdown_text:
|
| 77 |
+
markdown_text = str(ocr_response)
|
| 78 |
+
|
| 79 |
+
logger.info("Mistral OCR completed successfully")
|
| 80 |
+
return markdown_text
|
| 81 |
+
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.error(f"Mistral OCR error: {e}")
|
| 84 |
+
return f"Mistral OCR error: {e}"
|
| 85 |
+
|
| 86 |
+
def openai_ocr(image: Image.Image):
|
| 87 |
+
"""Process OCR using OpenAI's GPT-4o model."""
|
| 88 |
+
try:
|
| 89 |
+
# Convert image to base64
|
| 90 |
+
buffered = io.BytesIO()
|
| 91 |
+
image.save(buffered, format="PNG")
|
| 92 |
+
img_bytes = buffered.getvalue()
|
| 93 |
+
base64_image = base64.b64encode(img_bytes).decode('utf-8')
|
| 94 |
+
image_data_url = f"data:image/png;base64,{base64_image}"
|
| 95 |
+
|
| 96 |
+
# Send request to GPT-4o for OCR
|
| 97 |
+
response = openai.chat.completions.create(
|
| 98 |
+
model="gpt-4o",
|
| 99 |
+
messages=[
|
| 100 |
+
{
|
| 101 |
+
"role": "user",
|
| 102 |
+
"content": [
|
| 103 |
+
{"type": "text", "text": "Extract and transcribe all text from this image. Return only the transcribed text in markdown format, preserving any formatting like headers, lists, etc."},
|
| 104 |
+
{"type": "image_url", "image_url": {"url": image_data_url}}
|
| 105 |
+
]
|
| 106 |
+
}
|
| 107 |
+
]
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
markdown_text = response.choices[0].message.content
|
| 111 |
+
logger.info("OpenAI OCR completed successfully")
|
| 112 |
+
return markdown_text
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.error(f"OpenAI OCR error: {e}")
|
| 116 |
+
return f"OpenAI OCR error: {e}"
|
| 117 |
+
|
| 118 |
+
def gpt5_ocr(image: Image.Image):
|
| 119 |
+
"""Process OCR using OpenAI's GPT-5 model with the same prompt."""
|
| 120 |
+
try:
|
| 121 |
+
# Convert image to base64 (PNG) and use as data URL
|
| 122 |
+
buffered = io.BytesIO()
|
| 123 |
+
image.save(buffered, format="PNG")
|
| 124 |
+
img_bytes = buffered.getvalue()
|
| 125 |
+
base64_image = base64.b64encode(img_bytes).decode('utf-8')
|
| 126 |
+
image_data_url = f"data:image/png;base64,{base64_image}"
|
| 127 |
+
|
| 128 |
+
# Use Chat Completions style content for multimodal reliability
|
| 129 |
+
response = openai.chat.completions.create(
|
| 130 |
+
model="gpt-5",
|
| 131 |
+
messages=[
|
| 132 |
+
{
|
| 133 |
+
"role": "user",
|
| 134 |
+
"content": [
|
| 135 |
+
{"type": "text", "text": "Extract and transcribe all text from this image. Return only the transcribed text in markdown format, preserving any formatting like headers, lists, etc."},
|
| 136 |
+
{"type": "image_url", "image_url": {"url": image_data_url}}
|
| 137 |
+
]
|
| 138 |
+
}
|
| 139 |
+
]
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
markdown_text = response.choices[0].message.content
|
| 143 |
+
logger.info("GPT-5 OCR completed successfully")
|
| 144 |
+
return markdown_text
|
| 145 |
+
except Exception as e:
|
| 146 |
+
logger.error(f"GPT-5 OCR error: {e}")
|
| 147 |
+
return f"GPT-5 OCR error: {e}"
|
| 148 |
+
|
| 149 |
+
def process_model_ocr(image, model_name):
|
| 150 |
+
"""Process OCR for a specific model."""
|
| 151 |
+
if model_name == "gemini":
|
| 152 |
+
return gemini_ocr(image)
|
| 153 |
+
elif model_name == "mistral":
|
| 154 |
+
return mistral_ocr(image)
|
| 155 |
+
elif model_name == "openai":
|
| 156 |
+
return openai_ocr(image)
|
| 157 |
+
elif model_name == "gpt5":
|
| 158 |
+
return gpt5_ocr(image)
|
| 159 |
+
else:
|
| 160 |
+
return f"Unknown model: {model_name}"
|
| 161 |
+
|
| 162 |
+
# Initialize Gemini model
|
| 163 |
+
def initialize_gemini():
|
| 164 |
+
"""Initialize the Gemini model with API key."""
|
| 165 |
+
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
| 166 |
+
if gemini_api_key:
|
| 167 |
+
genai.configure(api_key=gemini_api_key)
|
| 168 |
+
logger.info("✅ GEMINI_API_KEY loaded successfully")
|
| 169 |
+
return genai.GenerativeModel('gemini-2.0-flash-exp')
|
| 170 |
+
else:
|
| 171 |
+
logger.error("❌ GEMINI_API_KEY not found in environment variables")
|
| 172 |
+
return None
|
| 173 |
+
|
| 174 |
+
# Initialize OpenAI
|
| 175 |
+
def initialize_openai():
|
| 176 |
+
"""Initialize OpenAI with API key."""
|
| 177 |
+
openai_api_key = os.getenv("OPENAI_API_KEY")
|
| 178 |
+
if openai_api_key:
|
| 179 |
+
openai.api_key = openai_api_key
|
| 180 |
+
logger.info("✅ OPENAI_API_KEY loaded successfully")
|
| 181 |
+
else:
|
| 182 |
+
logger.error("❌ OPENAI_API_KEY not found in environment variables")
|
| 183 |
+
|
| 184 |
+
# Initialize Mistral
|
| 185 |
+
def initialize_mistral():
|
| 186 |
+
"""Initialize Mistral with API key."""
|
| 187 |
+
mistral_api_key = os.getenv("MISTRAL_API_KEY")
|
| 188 |
+
if mistral_api_key:
|
| 189 |
+
logger.info("✅ MISTRAL_API_KEY loaded successfully")
|
| 190 |
+
else:
|
| 191 |
+
logger.error("❌ MISTRAL_API_KEY not found in environment variables")
|
ocr_votes.sql
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- New database schema for three-model OCR comparison system
|
| 2 |
+
-- This script creates a new table with model information
|
| 3 |
+
|
| 4 |
+
-- Drop the existing table if it exists
|
| 5 |
+
DROP TABLE IF EXISTS ocr_votes;
|
| 6 |
+
|
| 7 |
+
-- Create the new table with model information
|
| 8 |
+
CREATE TABLE ocr_votes (
|
| 9 |
+
id SERIAL PRIMARY KEY,
|
| 10 |
+
username VARCHAR(255) NOT NULL,
|
| 11 |
+
model_a VARCHAR(50) NOT NULL, -- 'gemini', 'mistral', or 'openai'
|
| 12 |
+
model_b VARCHAR(50) NOT NULL, -- 'gemini', 'mistral', or 'openai'
|
| 13 |
+
model_a_output TEXT NOT NULL,
|
| 14 |
+
model_b_output TEXT NOT NULL,
|
| 15 |
+
vote VARCHAR(50) NOT NULL, -- 'model_a' or 'model_b'
|
| 16 |
+
image_url TEXT,
|
| 17 |
+
timestamp VARCHAR(50) NOT NULL, -- Format: YYYY-MM-DD HH:MM:SS
|
| 18 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
+
-- Create indexes for better performance
|
| 22 |
+
CREATE INDEX idx_ocr_votes_username ON ocr_votes(username);
|
| 23 |
+
CREATE INDEX idx_ocr_votes_timestamp ON ocr_votes(timestamp);
|
| 24 |
+
CREATE INDEX idx_ocr_votes_vote ON ocr_votes(vote);
|
| 25 |
+
CREATE INDEX idx_ocr_votes_models ON ocr_votes(model_a, model_b);
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
pillow
|
| 3 |
+
mistralai
|
| 4 |
+
uvicorn
|
| 5 |
+
numpy<2
|
| 6 |
+
google-generativeai
|
| 7 |
+
supabase
|
| 8 |
+
python-dotenv
|
| 9 |
+
websockets==11.0.3
|
| 10 |
+
openai
|
storage.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Supabase storage helper for uploading files.
|
| 2 |
+
|
| 3 |
+
This module provides functions to upload files to Supabase storage
|
| 4 |
+
using HTTP requests, avoiding the dependency issues with the supabase client library.
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import requests
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Optional, Dict, Any
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
# Import Supabase credentials from environment variables
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
| 17 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
| 18 |
+
|
| 19 |
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
| 20 |
+
raise ImportError("Could not load Supabase credentials from environment variables")
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
# Supabase storage API configuration
|
| 25 |
+
STORAGE_BASE_URL = f"{SUPABASE_URL}/storage/v1"
|
| 26 |
+
HEADERS = {
|
| 27 |
+
"apikey": SUPABASE_KEY,
|
| 28 |
+
"Authorization": f"Bearer {SUPABASE_KEY}",
|
| 29 |
+
"Content-Type": "application/json"
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
def upload_file_to_bucket(
|
| 33 |
+
file_path: str,
|
| 34 |
+
bucket_name: str = "images",
|
| 35 |
+
storage_path: Optional[str] = None,
|
| 36 |
+
file_options: Optional[Dict[str, Any]] = None
|
| 37 |
+
) -> Dict[str, Any]:
|
| 38 |
+
"""
|
| 39 |
+
Upload a file to Supabase storage bucket.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
file_path: Local path to the file to upload
|
| 43 |
+
bucket_name: Name of the storage bucket (default: "images")
|
| 44 |
+
storage_path: Path in the bucket where to store the file (default: filename)
|
| 45 |
+
file_options: Optional file options like cache-control, upsert, etc.
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
Dictionary with upload result information
|
| 49 |
+
"""
|
| 50 |
+
try:
|
| 51 |
+
# Check if file exists
|
| 52 |
+
if not os.path.exists(file_path):
|
| 53 |
+
raise FileNotFoundError(f"File not found: {file_path}")
|
| 54 |
+
|
| 55 |
+
# Get file info
|
| 56 |
+
file_path_obj = Path(file_path)
|
| 57 |
+
file_name = file_path_obj.name
|
| 58 |
+
|
| 59 |
+
# Use provided storage_path or default to filename
|
| 60 |
+
if storage_path is None:
|
| 61 |
+
storage_path = file_name
|
| 62 |
+
|
| 63 |
+
# Prepare upload URL
|
| 64 |
+
upload_url = f"{STORAGE_BASE_URL}/object/{bucket_name}/{storage_path}"
|
| 65 |
+
|
| 66 |
+
# Prepare headers for file upload
|
| 67 |
+
upload_headers = {
|
| 68 |
+
"apikey": SUPABASE_KEY,
|
| 69 |
+
"Authorization": f"Bearer {SUPABASE_KEY}"
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
# Add file options if provided
|
| 73 |
+
if file_options:
|
| 74 |
+
for key, value in file_options.items():
|
| 75 |
+
upload_headers[f"x-upsert"] = str(value).lower() if key == "upsert" else str(value)
|
| 76 |
+
|
| 77 |
+
# Read and upload file
|
| 78 |
+
with open(file_path, "rb") as f:
|
| 79 |
+
response = requests.post(
|
| 80 |
+
upload_url,
|
| 81 |
+
headers=upload_headers,
|
| 82 |
+
data=f
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
if response.status_code == 200:
|
| 86 |
+
result = response.json()
|
| 87 |
+
logger.info(f"✅ File uploaded successfully: {file_name}")
|
| 88 |
+
logger.info(f"📁 Storage path: {storage_path}")
|
| 89 |
+
logger.info(f"🔗 Public URL: {result.get('publicUrl', 'N/A')}")
|
| 90 |
+
return {
|
| 91 |
+
"success": True,
|
| 92 |
+
"file_name": file_name,
|
| 93 |
+
"storage_path": storage_path,
|
| 94 |
+
"public_url": result.get('publicUrl'),
|
| 95 |
+
"response": result
|
| 96 |
+
}
|
| 97 |
+
else:
|
| 98 |
+
logger.error(f"❌ Upload failed with status {response.status_code}")
|
| 99 |
+
logger.error(f"Response: {response.text}")
|
| 100 |
+
return {
|
| 101 |
+
"success": False,
|
| 102 |
+
"error": f"Upload failed: {response.status_code}",
|
| 103 |
+
"response": response.text
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
except Exception as e:
|
| 107 |
+
logger.error(f"❌ Error uploading file: {e}")
|
| 108 |
+
return {
|
| 109 |
+
"success": False,
|
| 110 |
+
"error": str(e)
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
def get_file_url(bucket_name: str, file_path: str) -> str:
|
| 114 |
+
"""
|
| 115 |
+
Get the public URL for a file in Supabase storage.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
bucket_name: Name of the storage bucket
|
| 119 |
+
file_path: Path to the file in the bucket
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
Public URL for the file
|
| 123 |
+
"""
|
| 124 |
+
return f"{SUPABASE_URL}/storage/v1/object/public/{bucket_name}/{file_path}"
|
| 125 |
+
|
| 126 |
+
def list_bucket_files(bucket_name: str = "images") -> Dict[str, Any]:
|
| 127 |
+
"""
|
| 128 |
+
List all files in a storage bucket.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
bucket_name: Name of the storage bucket
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
Dictionary with list of files
|
| 135 |
+
"""
|
| 136 |
+
try:
|
| 137 |
+
list_url = f"{STORAGE_BASE_URL}/object/list/{bucket_name}"
|
| 138 |
+
response = requests.get(list_url, headers=HEADERS)
|
| 139 |
+
|
| 140 |
+
if response.status_code == 200:
|
| 141 |
+
result = response.json()
|
| 142 |
+
logger.info(f"✅ Listed {len(result)} files in bucket '{bucket_name}'")
|
| 143 |
+
return {
|
| 144 |
+
"success": True,
|
| 145 |
+
"files": result
|
| 146 |
+
}
|
| 147 |
+
else:
|
| 148 |
+
logger.error(f"❌ Failed to list files: {response.status_code}")
|
| 149 |
+
return {
|
| 150 |
+
"success": False,
|
| 151 |
+
"error": f"Failed to list files: {response.status_code}"
|
| 152 |
+
}
|
| 153 |
+
except Exception as e:
|
| 154 |
+
logger.error(f"❌ Error listing files: {e}")
|
| 155 |
+
return {
|
| 156 |
+
"success": False,
|
| 157 |
+
"error": str(e)
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
def test_upload_image_png():
|
| 161 |
+
"""Test function to upload Image.png to the images bucket."""
|
| 162 |
+
try:
|
| 163 |
+
logger.info("🚀 Testing file upload to Supabase storage...")
|
| 164 |
+
|
| 165 |
+
# First, test if we can list files (this tests basic connectivity)
|
| 166 |
+
logger.info("📋 Testing bucket connectivity...")
|
| 167 |
+
list_result = list_bucket_files("images")
|
| 168 |
+
if list_result["success"]:
|
| 169 |
+
logger.info(f"✅ Bucket connectivity successful! Found {len(list_result['files'])} files")
|
| 170 |
+
else:
|
| 171 |
+
logger.warning(f"⚠️ Bucket connectivity issue: {list_result['error']}")
|
| 172 |
+
|
| 173 |
+
# Test upload
|
| 174 |
+
logger.info("📤 Testing file upload...")
|
| 175 |
+
result = upload_file_to_bucket(
|
| 176 |
+
file_path="Image.png",
|
| 177 |
+
bucket_name="images",
|
| 178 |
+
storage_path="test/Image.png",
|
| 179 |
+
file_options={"cache-control": "3600", "upsert": "false"}
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
if result["success"]:
|
| 183 |
+
logger.info("✅ Upload test successful!")
|
| 184 |
+
logger.info(f"📁 File uploaded to: {result['storage_path']}")
|
| 185 |
+
logger.info(f"🔗 Public URL: {result['public_url']}")
|
| 186 |
+
return True
|
| 187 |
+
else:
|
| 188 |
+
logger.error(f"❌ Upload test failed: {result['error']}")
|
| 189 |
+
|
| 190 |
+
# Provide helpful error information
|
| 191 |
+
if "row-level security policy" in result.get('response', ''):
|
| 192 |
+
logger.error("🔧 This is a storage permissions issue.")
|
| 193 |
+
logger.error("📋 To fix this, you need to configure your Supabase storage bucket:")
|
| 194 |
+
logger.error("""
|
| 195 |
+
1. Go to your Supabase dashboard
|
| 196 |
+
2. Navigate to Storage > Policies
|
| 197 |
+
3. For the 'images' bucket, add a policy like:
|
| 198 |
+
|
| 199 |
+
CREATE POLICY "Allow public uploads" ON storage.objects
|
| 200 |
+
FOR INSERT WITH CHECK (bucket_id = 'images');
|
| 201 |
+
|
| 202 |
+
CREATE POLICY "Allow public reads" ON storage.objects
|
| 203 |
+
FOR SELECT USING (bucket_id = 'images');
|
| 204 |
+
|
| 205 |
+
Or make the bucket public in Storage > Settings
|
| 206 |
+
""")
|
| 207 |
+
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
except Exception as e:
|
| 211 |
+
logger.error(f"❌ Upload test error: {e}")
|
| 212 |
+
return False
|
ui_helpers.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
UI Helpers Module
|
| 3 |
+
Contains UI formatting and helper functions for the Gradio interface.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
import random
|
| 8 |
+
import math
|
| 9 |
+
from typing import Dict, Any, List
|
| 10 |
+
|
| 11 |
+
# Configure logging
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
def get_model_display_name(model_name: str) -> str:
|
| 15 |
+
"""Get the display name for a model."""
|
| 16 |
+
model_names = {
|
| 17 |
+
"gemini": "Gemini 2.0 Flash",
|
| 18 |
+
"mistral": "Mistral OCR",
|
| 19 |
+
"openai": "OpenAI GPT-4o",
|
| 20 |
+
"gpt5": "OpenAI GPT-5"
|
| 21 |
+
}
|
| 22 |
+
return model_names.get(model_name, model_name)
|
| 23 |
+
|
| 24 |
+
def select_random_models() -> tuple[str, str]:
|
| 25 |
+
"""Randomly select two models from the available list including gpt5."""
|
| 26 |
+
models = ["gemini", "mistral", "openai", "gpt5"]
|
| 27 |
+
selected_models = random.sample(models, 2)
|
| 28 |
+
return selected_models[0], selected_models[1]
|
| 29 |
+
|
| 30 |
+
def format_votes_table(votes: List[Dict[str, Any]]) -> str:
|
| 31 |
+
"""Format votes data into an HTML table with OCR outputs and image thumbnails."""
|
| 32 |
+
if not votes:
|
| 33 |
+
return "<p>No votes found in the database.</p>"
|
| 34 |
+
|
| 35 |
+
# Sort votes by timestamp (latest first)
|
| 36 |
+
sorted_votes = sorted(votes, key=lambda x: x.get('timestamp', ''), reverse=True)
|
| 37 |
+
|
| 38 |
+
html = """
|
| 39 |
+
<div style="overflow-x: auto; max-width: 100%;">
|
| 40 |
+
<table class="vote-table" style="width: 100%; table-layout: fixed; font-size: 12px;">
|
| 41 |
+
<thead>
|
| 42 |
+
<tr>
|
| 43 |
+
<th style="width: 12%;">Timestamp</th>
|
| 44 |
+
<th style="width: 8%;">Username</th>
|
| 45 |
+
<th style="width: 10%;">Models</th>
|
| 46 |
+
<th style="width: 8%;">Vote</th>
|
| 47 |
+
<th style="width: 25%;">Model A Output</th>
|
| 48 |
+
<th style="width: 25%;">Model B Output</th>
|
| 49 |
+
<th style="width: 12%;">Image</th>
|
| 50 |
+
</tr>
|
| 51 |
+
</thead>
|
| 52 |
+
<tbody>
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
for vote in sorted_votes:
|
| 56 |
+
timestamp = vote.get('timestamp', 'N/A')
|
| 57 |
+
username = vote.get('username', 'N/A')
|
| 58 |
+
model_a = vote.get('model_a', 'N/A')
|
| 59 |
+
model_b = vote.get('model_b', 'N/A')
|
| 60 |
+
vote_choice = vote.get('vote', 'N/A')
|
| 61 |
+
model_a_output = vote.get('model_a_output', 'N/A')
|
| 62 |
+
model_b_output = vote.get('model_b_output', 'N/A')
|
| 63 |
+
image_url = vote.get('image_url', 'N/A')
|
| 64 |
+
|
| 65 |
+
# Format timestamp - handle both ISO format and our custom format
|
| 66 |
+
if timestamp != 'N/A':
|
| 67 |
+
try:
|
| 68 |
+
from datetime import datetime
|
| 69 |
+
# Check if it's already in our desired format
|
| 70 |
+
if len(timestamp) == 19 and timestamp[10] == ' ':
|
| 71 |
+
# Already in YYYY-MM-DD HH:MM:SS format
|
| 72 |
+
formatted_time = timestamp
|
| 73 |
+
else:
|
| 74 |
+
# Convert from ISO format to our format
|
| 75 |
+
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
| 76 |
+
formatted_time = dt.strftime('%Y-%m-%d %H:%M:%S')
|
| 77 |
+
except:
|
| 78 |
+
formatted_time = timestamp
|
| 79 |
+
else:
|
| 80 |
+
formatted_time = 'N/A'
|
| 81 |
+
|
| 82 |
+
# Get model display names
|
| 83 |
+
model_a_name = get_model_display_name(model_a)
|
| 84 |
+
model_b_name = get_model_display_name(model_b)
|
| 85 |
+
models_display = f"{model_a_name} vs {model_b_name}"
|
| 86 |
+
|
| 87 |
+
# Determine which model was voted for and get its display name
|
| 88 |
+
voted_model_name = ""
|
| 89 |
+
vote_color = "gray"
|
| 90 |
+
if vote_choice == "model_a":
|
| 91 |
+
voted_model_name = model_a_name
|
| 92 |
+
vote_color = "green"
|
| 93 |
+
elif vote_choice == "model_b":
|
| 94 |
+
voted_model_name = model_b_name
|
| 95 |
+
vote_color = "blue"
|
| 96 |
+
|
| 97 |
+
# Truncate OCR outputs for table display (shorter for better fit)
|
| 98 |
+
model_a_preview = model_a_output[:80] + "..." if len(model_a_output) > 80 else model_a_output
|
| 99 |
+
model_b_preview = model_b_output[:80] + "..." if len(model_b_output) > 80 else model_b_output
|
| 100 |
+
|
| 101 |
+
# Fix image URL - use the correct Supabase storage URL format
|
| 102 |
+
if image_url and image_url != 'N/A' and not image_url.startswith('http'):
|
| 103 |
+
# If it's just a path, construct the full URL
|
| 104 |
+
import os
|
| 105 |
+
image_url = f"{os.getenv('SUPABASE_URL')}/storage/v1/object/public/images/{image_url}"
|
| 106 |
+
|
| 107 |
+
# Create image thumbnail or placeholder
|
| 108 |
+
if image_url and image_url != 'N/A':
|
| 109 |
+
image_html = f'<img src="{image_url}" alt="OCR Image" style="width: 60px; height: 45px; object-fit: cover; border-radius: 4px; cursor: pointer;" onclick="window.open(\'{image_url}\', \'_blank\')" title="Click to view full image">'
|
| 110 |
+
else:
|
| 111 |
+
image_html = '<span style="color: #999; font-style: italic;">No image</span>'
|
| 112 |
+
|
| 113 |
+
html += f"""
|
| 114 |
+
<tr>
|
| 115 |
+
<td style="word-wrap: break-word; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{formatted_time}</td>
|
| 116 |
+
<td style="word-wrap: break-word; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><strong>{username}</strong></td>
|
| 117 |
+
<td style="word-wrap: break-word; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><small>{models_display}</small></td>
|
| 118 |
+
<td style="color: {vote_color}; font-weight: bold; word-wrap: break-word; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{voted_model_name}</td>
|
| 119 |
+
<td style="word-wrap: break-word; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="{model_a_output}">{model_a_preview}</td>
|
| 120 |
+
<td style="word-wrap: break-word; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="{model_b_output}">{model_b_preview}</td>
|
| 121 |
+
<td style="text-align: center;">{image_html}</td>
|
| 122 |
+
</tr>
|
| 123 |
+
"""
|
| 124 |
+
|
| 125 |
+
html += """
|
| 126 |
+
</tbody>
|
| 127 |
+
</table>
|
| 128 |
+
</div>
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
return html
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def format_elo_leaderboard(elo_ratings: Dict[str, float], vote_counts: Dict[str, int] = None) -> str:
|
| 136 |
+
"""Format ELO ratings into a leaderboard HTML table."""
|
| 137 |
+
# Sort models by ELO rating (highest first)
|
| 138 |
+
sorted_models = sorted(elo_ratings.items(), key=lambda x: x[1], reverse=True)
|
| 139 |
+
|
| 140 |
+
html = """
|
| 141 |
+
<div style="padding: 15px; background-color: #f8f9fa; border-radius: 8px;">
|
| 142 |
+
<h3>ELO Leaderboard</h3>
|
| 143 |
+
<p><em>Models are ranked by their ELO rating. Higher ratings indicate better performance.</em></p>
|
| 144 |
+
|
| 145 |
+
<table class="vote-table" style="margin-top: 15px;">
|
| 146 |
+
<thead>
|
| 147 |
+
<tr>
|
| 148 |
+
<th>Rank</th>
|
| 149 |
+
<th>Model</th>
|
| 150 |
+
<th>ELO Rating</th>
|
| 151 |
+
<th>Total Votes</th>
|
| 152 |
+
</tr>
|
| 153 |
+
</thead>
|
| 154 |
+
<tbody>
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
for rank, (model, rating) in enumerate(sorted_models, 1):
|
| 158 |
+
# Get model display name
|
| 159 |
+
display_name = get_model_display_name(model)
|
| 160 |
+
|
| 161 |
+
# Get vote count for this model
|
| 162 |
+
vote_count = vote_counts.get(model, 0) if vote_counts else 0
|
| 163 |
+
|
| 164 |
+
html += f"""
|
| 165 |
+
<tr>
|
| 166 |
+
<td style="font-weight: bold; text-align: center;">{rank}</td>
|
| 167 |
+
<td><strong>{display_name}</strong></td>
|
| 168 |
+
<td style="font-weight: bold;">{rating:.0f}</td>
|
| 169 |
+
<td style="text-align: center;">{vote_count}</td>
|
| 170 |
+
</tr>
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
html += """
|
| 174 |
+
</tbody>
|
| 175 |
+
</table>
|
| 176 |
+
</div>
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
return html
|