Wassymk commited on
Commit
c63c0e5
·
1 Parent(s): 8a5379a
README.md CHANGED
@@ -1,12 +1,14 @@
1
  ---
2
- title: OCRArena
3
- emoji:
4
- colorFrom: yellow
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 5.44.1
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