1163 lines
47 KiB
Python
1163 lines
47 KiB
Python
#### To launch the script
|
|
# gradio wcag_validator_ui.py
|
|
# python wcag_validator_ui.py
|
|
|
|
import gradio as gr
|
|
from gradio_modal import Modal
|
|
import requests
|
|
from pathlib import Path
|
|
import sys
|
|
import pandas as pd
|
|
|
|
parent_dir = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(parent_dir))
|
|
from dotenv import load_dotenv, find_dotenv
|
|
from dependences.utils import (
|
|
call_API_urlibrequest,
|
|
create_folder,
|
|
db_persistence_startup,
|
|
db_persistence_insert,
|
|
return_from_env_valid,
|
|
|
|
)
|
|
|
|
from dependences_ui.utils import *
|
|
import logging
|
|
import time
|
|
import json
|
|
import urllib.request
|
|
import urllib.parse
|
|
import os
|
|
import sqlite3
|
|
|
|
|
|
|
|
from user_task_assignment.user_assignment_manager import UserAssignmentManager
|
|
from dependences_ui.utils import load_users,get_user_assessments_done
|
|
|
|
def user_assigment_management():
|
|
users=load_users()
|
|
user_list=list(users.keys())
|
|
print(f"Loaded users from simple JSON: {len(user_list)}")
|
|
|
|
user_assignment_manager = UserAssignmentManager(
|
|
db_path="persistence/wcag_validator_ui.db",
|
|
config_json_path="user_task_assignment/sites_config.json",
|
|
assignments_json_path="user_task_assignment/alt_text_assignments_output_target_overlap.json",
|
|
assignments_xlsx_path="user_task_assignment/alt_text_assignments_output_target_overlap.xlsx",
|
|
|
|
)
|
|
|
|
# Get current managed users
|
|
managed_users_number = user_assignment_manager.get_managed_user_count()
|
|
print(f"Currently managed users from db: {managed_users_number}")
|
|
if managed_users_number !=len(user_list):# rigenenerate files only if some user numbers disalignmnets. Avoid only updates on new user registration process
|
|
print(f"Warning: Number of users in db ({managed_users_number}) does not match number of users loaded from JSON ({len(user_list)}). Re-init user assignments files.")
|
|
user_assignment_manager.register_active_users(user_list)#on startup register users loaded from JSON into the manager (creating also assignments .json amd .xml files)
|
|
# Get current managed users after regsitration alignment
|
|
managed_users_number = user_assignment_manager.get_managed_user_count()
|
|
print(f"Currently managed users from db after alignment: {managed_users_number}")
|
|
|
|
# Get current managed users after regsitration alignment
|
|
|
|
print(f"Total managed users from db: {managed_users_number}\n")
|
|
if managed_users_number !=len(user_list):
|
|
print(f"Warning: Number of users in db ({managed_users_number}) does not match number of users loaded from JSON ({len(user_list)}). Check user assignment manager initialization.")
|
|
exit(1)
|
|
|
|
user_assignment_stats = user_assignment_manager.get_statistics()
|
|
print(f"Current assignment stats:{user_assignment_stats} \n")
|
|
return user_assignment_manager
|
|
|
|
|
|
|
|
WCAG_VALIDATOR_RESTSERVER_HEADERS = [("Content-Type", "application/json")]
|
|
|
|
|
|
def maybe_close_modal(process_dataframe_output_state):
|
|
print("Checking if modal can be closed based on:",type(process_dataframe_output_state), process_dataframe_output_state)
|
|
if not process_dataframe_output_state:
|
|
print("Modal cannot be closed.")
|
|
return Modal(visible=True) # keep it open
|
|
return Modal(visible=False) # close it
|
|
|
|
def maybe_open_modal(make_alttext_llm_assessment_api_call_output_state):
|
|
print("Checking if modal can be opened based on:",type(make_alttext_llm_assessment_api_call_output_state), make_alttext_llm_assessment_api_call_output_state)
|
|
if not make_alttext_llm_assessment_api_call_output_state:
|
|
print("Modal cannot be opened.")
|
|
return Modal(visible=False)
|
|
return Modal(visible=True)
|
|
|
|
def render_user_assessmnet_status_table(df):
|
|
if df is None or df.empty:
|
|
return "<p>No assignments found.</p>"
|
|
|
|
total_work_to_be_done=[]
|
|
rows = ""
|
|
for _, row in df.iterrows():
|
|
url = row["Website URL"]
|
|
assigned = row["Assigned Image Number"]
|
|
work_done = row["Work Done on Image Number"]
|
|
work_to_be_done = [img for img in assigned if img not in work_done]
|
|
total_work_to_be_done+=work_to_be_done
|
|
|
|
rows += f"""
|
|
<tr>
|
|
<td style="padding:8px; border:1px solid #ddd; word-break:break-all;">
|
|
<a href="{url}" target="_blank">{url}</a>
|
|
</td>
|
|
<td style="padding:8px; border:1px solid #ddd; text-align:center;">
|
|
{assigned}
|
|
</td>
|
|
<td style="padding:8px; border:1px solid #ddd; text-align:center;">
|
|
{work_done}
|
|
</td>
|
|
<td style="padding:8px; border:1px solid #ddd; text-align:center;">
|
|
{work_to_be_done}
|
|
</td>
|
|
</tr>
|
|
"""
|
|
total_work_to_be_done_text =""
|
|
if len(total_work_to_be_done)==0:
|
|
total_work_to_be_done_text="All assigned work is done! Great job!"
|
|
|
|
return f"""
|
|
<div style="overflow-x:auto;">
|
|
<table style="width:100%; border-collapse:collapse; font-size:14px;">
|
|
<thead>
|
|
<tr style="background-color:#f2f2f2;">
|
|
<th style="padding:10px; border:1px solid #ddd; text-align:left;">Website URL</th>
|
|
<th style="padding:10px; border:1px solid #ddd; text-align:left;">Assigned Image Number</th>
|
|
<th style="padding:10px; border:1px solid #ddd; text-align:left;">Work Done on Image Number</th>
|
|
<th style="padding:10px; border:1px solid #ddd; text-align:left;">Work Still to be Done</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p style="color:green;font-size: large;font-weight: bold;">{total_work_to_be_done_text}</p>
|
|
"""
|
|
|
|
def display_user_assignment(db_path,user_state):
|
|
if user_state and "username" in user_state:
|
|
username = user_state["username"]
|
|
connection_db = sqlite3.connect(db_path)
|
|
|
|
user_assessment_work=get_user_assessments_done(connection_db ,username)
|
|
print(f"User {username} has done assessments for {user_assessment_work} images.")
|
|
|
|
|
|
print(f"Fetching assignment for user: {username}")
|
|
assignments = user_assignment_manager.get_user_assignments(username, from_user_name=True)
|
|
|
|
if assignments is not None:
|
|
print (f"Your current assignment: {assignments}")
|
|
|
|
else:
|
|
#return "No assignments found for you. Please contact the administrator."
|
|
return pd.DataFrame()
|
|
data_frame = []
|
|
for url in assignments :
|
|
#print(f"URL: {url}, Assigned Image List: {assignments[url]}")
|
|
data_frame.append(
|
|
{
|
|
"Website URL": url,
|
|
"Assigned Image Number": assignments[url],
|
|
"Work Done on Image Number":user_assessment_work[url] if url in user_assessment_work else [],
|
|
}
|
|
)
|
|
|
|
df = pd.DataFrame(data_frame)
|
|
#print(f"DataFrame to display for user {username}:\n{df}")
|
|
return df
|
|
else:
|
|
#return "User not logged in."
|
|
return pd.DataFrame()
|
|
|
|
def process_dataframe(db_path, url, updated_df, user_state={},llm_response_output={}):
|
|
|
|
print("Processing dataframe to adjust columns...type:",type(updated_df),updated_df)
|
|
# accept different input forms from UI (DataFrame, JSON string, or list of dicts)
|
|
try:
|
|
|
|
if isinstance(updated_df, str):
|
|
try:
|
|
updated_df = pd.read_json(updated_df, orient="records")
|
|
except Exception:
|
|
updated_df = pd.read_json(updated_df)
|
|
elif isinstance(updated_df, list):
|
|
updated_df = pd.DataFrame(updated_df)
|
|
except Exception as e:
|
|
return f"Error parsing updated data: {str(e)}" ,False
|
|
for column_rating_name in ["User Assessment for LLM Proposal 1", "User Assessment for LLM Proposal 2"]:
|
|
|
|
# Get the assessment column
|
|
try:
|
|
updated_df[column_rating_name] = updated_df[column_rating_name].astype(int)
|
|
except ValueError:
|
|
return "Error: User Assessment for LLM Proposal must be an integer",False
|
|
except KeyError:
|
|
return f"No data Saved because some images are not correcly managed. Please retry." ,False
|
|
except Exception as e:
|
|
return f"Error processing User Assessment for LLM Proposal: {str(e)}" ,False
|
|
|
|
if (updated_df[column_rating_name] < 1).any() or (
|
|
updated_df[column_rating_name] > 5
|
|
).any():
|
|
return "Error: User Assessment for LLM Proposal must be between 1 and 5",False
|
|
|
|
dataframe_json = updated_df.to_json(orient="records")
|
|
connection_db = sqlite3.connect(db_path)
|
|
json_user_str = json.dumps({"username": user_state["username"]}, ensure_ascii=False)
|
|
|
|
llm_response_output_str = json.dumps(llm_response_output, ensure_ascii=False) #recuperato dalla chiamata all'llm, ho tutte le info anche sulle immagini
|
|
try:
|
|
# insert after everything to keep datetime aligned
|
|
db_persistence_insert(
|
|
connection_db=connection_db,
|
|
insert_type="wcag_user_llm_alttext_assessments",
|
|
page_url=url,
|
|
user=json_user_str,
|
|
llm_model="",
|
|
json_in_str=llm_response_output_str,#dataframe_json,
|
|
json_out_str=dataframe_json,
|
|
table="wcag_user_assessments",
|
|
)
|
|
except Exception as e:
|
|
print("Error inserting user assessment into database:", str(e))
|
|
finally:
|
|
if connection_db:
|
|
connection_db.close()
|
|
print("User assessment saved to database successfully.returning:", True)
|
|
return "User assessment saved successfully!",True
|
|
|
|
|
|
def load_images_from_json(json_input,user_assignment_current_status_df):
|
|
"""Extract URLs and alt text from JSON and create HTML gallery"""
|
|
|
|
if user_assignment_current_status_df is None or user_assignment_current_status_df.empty:
|
|
print("No user assignment status found. Displaying all images without assignment info.")
|
|
|
|
user_assignments={}
|
|
for _, row in user_assignment_current_status_df.iterrows():
|
|
url = row["Website URL"]
|
|
assigned = row["Assigned Image Number"]
|
|
work_done = row["Work Done on Image Number"]
|
|
user_assignments[url] = {
|
|
"assigned": assigned,
|
|
"work_done": work_done
|
|
}
|
|
#print(f"User assignments extracted for image loading: {user_assignments}")
|
|
|
|
try:
|
|
data = json_input
|
|
|
|
if "images" not in data or not data["images"]:
|
|
return "No images found in JSON", ""
|
|
|
|
images = data["images"]
|
|
info_text = f"Found {len(images)} image(s)"
|
|
|
|
|
|
# Create HTML gallery with checkboxes and assessment forms
|
|
html = """
|
|
<style>
|
|
.image-gallery {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 20px;
|
|
padding: 20px;
|
|
}
|
|
.image-card {
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
background: white;
|
|
}
|
|
.image-card:has(input[type="checkbox"]:checked) {
|
|
border-color: #2196F3;
|
|
background: #a7c1c1;
|
|
}
|
|
.image-card img {
|
|
width: 100%;
|
|
height: 200px;
|
|
object-fit: scale-down;
|
|
border-radius: 4px;
|
|
}
|
|
.image-info {
|
|
margin-top: 10px;
|
|
}
|
|
.checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
}
|
|
.image-checkbox {
|
|
width: 18px;
|
|
height: 18px;
|
|
cursor: pointer;
|
|
accent-color: #2196F3;
|
|
}
|
|
.alt-text {
|
|
font-size: 14px;
|
|
color: #666;
|
|
margin-top: 5px;
|
|
}
|
|
.assessment-panel {
|
|
display: none;
|
|
margin-top: 15px;
|
|
padding: 10px;
|
|
background: aliceblue;
|
|
border-radius: 4px;
|
|
border: 1px solid #2196F3;
|
|
}
|
|
.assessment-panel.visible {
|
|
display: block;
|
|
}
|
|
.form-group {
|
|
margin: 10px 0;
|
|
}
|
|
.form-group label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: 5px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.radio-container {
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
}
|
|
|
|
.radio-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.radio-label {
|
|
font-weight: 500;
|
|
}
|
|
textarea {
|
|
width: 100%;
|
|
padding: 8px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
resize: vertical;
|
|
}
|
|
</style>
|
|
<div class="image-gallery">
|
|
"""
|
|
|
|
for idx, img_data in enumerate(images):
|
|
url = img_data.get("url", "")
|
|
alt_text = img_data.get("alt_text", "No description")
|
|
|
|
page_url = img_data.get("page_url", "")
|
|
assigned=user_assignments.get(page_url,{}).get("assigned",[])
|
|
work_done=user_assignments.get(page_url,{}).get("work_done",[])
|
|
assigned_text=""
|
|
if idx+1 in assigned:
|
|
assigned_text="-<span style='font-weight: bold'>(Assigned)</span>"
|
|
if idx+1 in work_done:
|
|
assigned_text+="-><span style='font-weight: bold'>(Already managed)</span>"
|
|
if idx+1 in assigned and idx+1 in work_done:
|
|
assigned_text+="<span style='font-family: wingdings; font-size: large; font-weight: bold; color:green'>ü</span>"
|
|
|
|
html += f"""
|
|
<div class="image-card">
|
|
<img src="{url}" alt="{alt_text}" loading="lazy" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22200%22%3E%3Crect fill=%22%23ddd%22 width=%22200%22 height=%22200%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22%3EImage not found%3C/text%3E%3C/svg%3E'">
|
|
<div class="image-info">
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" class="image-checkbox" data-imgurl="{url}" data-index="{idx}"
|
|
onchange="
|
|
const panel = document.getElementById('panel-{idx}');
|
|
const checkedCount = document.querySelectorAll('.image-checkbox:checked').length;
|
|
if (this.checked) {{
|
|
if (checkedCount > 6) {{
|
|
this.checked = false;
|
|
alert('Maximum 6 images can be selected!');
|
|
return;
|
|
}}
|
|
panel.classList.add('visible');
|
|
}} else {{
|
|
panel.classList.remove('visible');
|
|
}}
|
|
">
|
|
Select #{idx + 1}<span>{assigned_text}</span>
|
|
</label>
|
|
<div class="alt-text">Current alt_text: {alt_text}</div>
|
|
|
|
<div id="panel-{idx}" class="assessment-panel">
|
|
<div class="form-group">
|
|
<label>Rate current alt-text:</label>
|
|
<div class="radio-container">
|
|
<label class="radio-option">
|
|
<input type="radio" name="assessment-{idx}" value="1" data-index="{idx}">
|
|
<span class="radio-label">1</span>
|
|
</label>
|
|
<label class="radio-option">
|
|
<input type="radio" name="assessment-{idx}" value="2" data-index="{idx}">
|
|
<span class="radio-label">2</span>
|
|
</label>
|
|
<label class="radio-option">
|
|
<input type="radio" name="assessment-{idx}" value="3" data-index="{idx}">
|
|
<span class="radio-label">3</span>
|
|
</label>
|
|
<label class="radio-option">
|
|
<input type="radio" name="assessment-{idx}" value="4" data-index="{idx}">
|
|
<span class="radio-label">4</span>
|
|
</label>
|
|
<label class="radio-option">
|
|
<input type="radio" name="assessment-{idx}" value="5" data-index="{idx}">
|
|
<span class="radio-label">5</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>New alt-text:</label>
|
|
<textarea class="new-alt-text" data-index="{idx}" rows="3" placeholder="Enter improved alt-text...">{alt_text}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="hidden" class="original-alt" data-index="{idx}" value="{alt_text}" />
|
|
</div>
|
|
</div>
|
|
"""
|
|
# info_text += f"✓ Image {idx+1} alt_text: {alt_text}\n"
|
|
html += "</div>"
|
|
|
|
return info_text, html
|
|
|
|
except json.JSONDecodeError as e:
|
|
return f"Error: Invalid JSON format - {str(e)}", ""
|
|
except Exception as e:
|
|
return f"Error: {str(e)}", ""
|
|
|
|
|
|
def load_llm_assessment_from_json(json_input):
|
|
|
|
try:
|
|
# Parse JSON input
|
|
data = json_input
|
|
|
|
if "mllm_validations" not in data or not data["mllm_validations"]:
|
|
print("no mllm_validations found")
|
|
return pd.DataFrame()
|
|
|
|
if (
|
|
data["mllm_validations"]["mllm_alttext_assessments"].get("mllm_alttext_assessments_openai")
|
|
|
|
and data["mllm_validations"]["mllm_alttext_assessments"].get("mllm_alttext_assessments_local")
|
|
|
|
):
|
|
|
|
is_single_model_output = False
|
|
info_text = f"Assessment done by {len(data['mllm_validations']['mllm_alttext_assessments'])} models on {len(data['mllm_validations']['mllm_alttext_assessments']['mllm_alttext_assessments_openai'])} image(s)\n\n"
|
|
print(
|
|
f"The response contains multiple models output. Assessment done by {len(data['mllm_validations']['mllm_alttext_assessments'])} models on {len(data['mllm_validations']['mllm_alttext_assessments']['mllm_alttext_assessments_openai'])} image(s)"
|
|
)
|
|
|
|
else:
|
|
|
|
is_single_model_output = True
|
|
info_text = f"Assessment done on {len(data['mllm_validations']['mllm_alttext_assessments'])} image(s)\n\n"
|
|
print(
|
|
f"The response contains only one output. Assessment done on {len(data['mllm_validations']['mllm_alttext_assessments'])} image(s)"
|
|
)
|
|
|
|
data_frame = []
|
|
if is_single_model_output:
|
|
for idx, img_data in enumerate(
|
|
data["mllm_validations"]["mllm_alttext_assessments"], 1
|
|
):
|
|
|
|
original_alt_text_assessment = img_data["mllm_response"].get(
|
|
"original_alt_text_assessment", "No description"
|
|
)
|
|
new_alt_text = img_data["mllm_response"].get(
|
|
"new_alt_text", "No description"
|
|
)
|
|
alt_text_original = img_data.get("alt_text", "No alt_text provided")
|
|
|
|
data_frame.append(
|
|
{
|
|
"Original Alt Text": alt_text_original,
|
|
"LLM Assessment": original_alt_text_assessment,
|
|
"LLM Proposed Alt Text": new_alt_text,
|
|
}
|
|
)
|
|
else:
|
|
|
|
for idx, img_data in enumerate(
|
|
data["mllm_validations"]["mllm_alttext_assessments"]["mllm_alttext_assessments_openai"], 1
|
|
):
|
|
|
|
original_alt_text_assessment = img_data["mllm_response"].get(
|
|
"original_alt_text_assessment", "No description"
|
|
)
|
|
new_alt_text = img_data["mllm_response"].get(
|
|
"new_alt_text", "No description"
|
|
)
|
|
alt_text_original = img_data.get("alt_text", "No alt_text provided")
|
|
|
|
"""data_frame.append(
|
|
{
|
|
"Original Alt Text": alt_text_original,
|
|
"LLM Assessment": original_alt_text_assessment,
|
|
"LLM Proposed Alt Text": new_alt_text,
|
|
}
|
|
)"""
|
|
#for idx, img_data in enumerate(
|
|
# data["mllm_validations"]["mllm_alttext_assessments"]["mllm_alttext_assessments_local"], 1
|
|
#):
|
|
img_data_local = data["mllm_validations"]["mllm_alttext_assessments"]["mllm_alttext_assessments_local"][idx-1]
|
|
original_alt_text_assessment_local = img_data_local["mllm_response"].get(
|
|
"original_alt_text_assessment", "No description"
|
|
)
|
|
new_alt_text_local = img_data_local["mllm_response"].get(
|
|
"new_alt_text", "No description"
|
|
)
|
|
#alt_text_original = img_data.get("alt_text", "No alt_text provided")
|
|
|
|
data_frame.append(
|
|
{
|
|
"Original Alt Text": alt_text_original,
|
|
"LLM Assessment 1": original_alt_text_assessment,
|
|
"LLM Proposed Alt Text 1": new_alt_text,
|
|
"LLM Assessment 2": original_alt_text_assessment_local,
|
|
"LLM Proposed Alt Text 2": new_alt_text_local,
|
|
}
|
|
)
|
|
|
|
df = pd.DataFrame(data_frame)
|
|
|
|
return df
|
|
|
|
except json.JSONDecodeError as e:
|
|
return f"Error: Invalid JSON format - {str(e)}", []
|
|
except Exception as e:
|
|
return f"Error: {str(e)}", []
|
|
|
|
|
|
def make_alttext_llm_assessment_api_call(
|
|
url,
|
|
selected_images_json=[],
|
|
db_path=None,
|
|
wcag_rest_server_url="http://localhost:8000",
|
|
user_state={},
|
|
number_of_images=30,
|
|
):
|
|
|
|
print(
|
|
f"Making API call for llm assessment for {url} to {wcag_rest_server_url}/wcag_alttext_validation"
|
|
)
|
|
selected_images = json.loads(selected_images_json) if selected_images_json else []
|
|
# print("selected_images:", selected_images)
|
|
|
|
if not selected_images or len(selected_images) == 0:
|
|
info_text = "No images selected"
|
|
print("LLM assessment not started because no valid images were selected.")
|
|
return "LLM assessment not started", pd.DataFrame(), {},False
|
|
|
|
# prepare data for insertion
|
|
json_in_str = {}
|
|
json_out_str = {}
|
|
selected_urls = []
|
|
selected_alt_text_original = []
|
|
user_assessments = []
|
|
user_new_alt_texts = []
|
|
selected_image_id = []
|
|
user_assessments_llm_proposal_1 = []
|
|
user_assessments_llm_proposal_2 = []
|
|
|
|
for img in selected_images:
|
|
selected_urls.append(img["image_url"])
|
|
selected_alt_text_original.append(img["original_alt_text"])
|
|
user_assessments.append(img["assessment"])
|
|
user_new_alt_texts.append(img["new_alt_text"])
|
|
selected_image_id.append(
|
|
int(img["image_index"]) + 1
|
|
) # add the id selected (+1 for index alignment)
|
|
user_assessments_llm_proposal_1.append(0) # default value for now
|
|
user_assessments_llm_proposal_2.append(0) # default value for now
|
|
json_in_str["images_urls"] = selected_urls
|
|
json_in_str["images_alt_text_original"] = selected_alt_text_original
|
|
json_out_str["user_assessments"] = user_assessments
|
|
json_out_str["user_new_alt_texts"] = user_new_alt_texts
|
|
json_in_str = json.dumps(json_in_str, ensure_ascii=False)
|
|
json_out_str = json.dumps(json_out_str, ensure_ascii=False)
|
|
json_user_str = json.dumps({"username": user_state["username"]}, ensure_ascii=False)
|
|
connection_db = sqlite3.connect(db_path)
|
|
# ---------
|
|
|
|
try:
|
|
|
|
response = call_API_urlibrequest(
|
|
data={
|
|
"page_url": url,
|
|
"number_of_images": number_of_images,
|
|
"context_levels": 5,
|
|
"pixel_distance_threshold": 200,
|
|
"save_images": "True",
|
|
"save_elaboration": "True",
|
|
"specific_images_urls": selected_urls,
|
|
},
|
|
url=wcag_rest_server_url + "/wcag_alttext_validation",
|
|
headers=WCAG_VALIDATOR_RESTSERVER_HEADERS,
|
|
)
|
|
# return response
|
|
info_dataframe = load_llm_assessment_from_json(response)
|
|
#print("info_dataframe:", info_dataframe)
|
|
|
|
# add the UI ids and other fields to to api response
|
|
info_dataframe.insert(
|
|
0, "Image #", selected_image_id
|
|
) # add the UI ids from to api response
|
|
info_dataframe.insert(2, "User Assessment", user_assessments)
|
|
|
|
info_dataframe.insert(3, "User Proposed Alt Text", user_new_alt_texts)
|
|
info_dataframe["User Assessment for LLM Proposal 1"] = (
|
|
user_assessments_llm_proposal_1
|
|
)
|
|
info_dataframe["User Assessment for LLM Proposal 2"] = (
|
|
user_assessments_llm_proposal_2
|
|
)
|
|
#print("info_dataframe after adding user assessments:", info_dataframe)
|
|
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
try:
|
|
# insert after everything to keep datetime aligned
|
|
db_persistence_insert(
|
|
connection_db=connection_db,
|
|
insert_type="wcag_user_alttext_assessments",
|
|
page_url=url,
|
|
user=json_user_str,
|
|
llm_model="",
|
|
json_in_str=json_in_str,
|
|
json_out_str=json_out_str,
|
|
table="wcag_user_assessments",
|
|
)
|
|
except Exception as e:
|
|
print("Error inserting user assessment into database:", str(e))
|
|
finally:
|
|
if connection_db:
|
|
connection_db.close()
|
|
return "LLM assessment completed", info_dataframe, response, True
|
|
|
|
|
|
def make_image_extraction_api_call(
|
|
url,
|
|
number_of_images=30,
|
|
wcag_rest_server_url="http://localhost:8000",
|
|
user_assignment_current_status={},
|
|
):
|
|
print(
|
|
f"Making API call for image_extraction for {url} to {wcag_rest_server_url}/extract_images"
|
|
)
|
|
try:
|
|
|
|
response = call_API_urlibrequest(
|
|
data={
|
|
"page_url": url,
|
|
"number_of_images": number_of_images,
|
|
},
|
|
url=wcag_rest_server_url + "/extract_images",
|
|
headers=WCAG_VALIDATOR_RESTSERVER_HEADERS,
|
|
)
|
|
# return response
|
|
info_text, gallery_images = load_images_from_json(response,user_assignment_current_status)
|
|
|
|
return info_text, gallery_images
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
def render_alttext_form(df):
|
|
"""Render a pandas DataFrame (or list/dict) into an editable HTML form."""
|
|
try:
|
|
if df is None or df.empty:
|
|
print("No data to render in form.")
|
|
html=""
|
|
return gr.update(value=html), html # return empty form
|
|
if isinstance(df, str):
|
|
df = pd.read_json(df, orient="records")
|
|
if isinstance(df, dict):
|
|
df = pd.DataFrame(df)
|
|
if isinstance(df, list):
|
|
df = pd.DataFrame(df)
|
|
|
|
html = """
|
|
<style>
|
|
.alttext-table { width:100%; border-collapse: collapse; }
|
|
.alttext-table th, .alttext-table td { border:1px solid #ddd; padding:8px; }
|
|
.alttext-table th { background:#f5f5f5; }
|
|
.alttext-row td { vertical-align: top; }
|
|
.llm-select { width:auto; }
|
|
</style>
|
|
<table class="alttext-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Image #</th>
|
|
<th>Original Alt Text</th>
|
|
<th>User Assessment</th>
|
|
<th>User Proposed Alt Text</th>
|
|
<th>LLM Assessment 1</th>
|
|
<th>LLM Proposed Alt Text 1</th>
|
|
<th>User Assessment for LLM Proposal 1</th>
|
|
<th>LLM Assessment 2</th>
|
|
<th>LLM Proposed Alt Text 2</th>
|
|
<th>User Assessment for LLM Proposal 2</th>
|
|
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
"""
|
|
|
|
for _, row in df.iterrows():
|
|
imgnum = row.get("Image #", "")
|
|
orig = row.get("Original Alt Text", "")
|
|
user_ass = row.get("User Assessment", "")
|
|
user_prop = row.get("User Proposed Alt Text", "")
|
|
llm1_ass = row.get("LLM Assessment 1", "")
|
|
llm2_ass = row.get("LLM Assessment 2", "")
|
|
llm1_prop = row.get("LLM Proposed Alt Text 1", "")
|
|
llm2_prop = row.get("LLM Proposed Alt Text 2", "")
|
|
|
|
user_llm1_ass = row.get("User Assessment for LLM Proposal 1", 0)
|
|
user_llm2_ass = row.get("User Assessment for LLM Proposal 2", 0)
|
|
|
|
html += f"""
|
|
<tr class="alttext-row" data-index="{imgnum}">
|
|
<td class="img-num">{imgnum}</td>
|
|
<td class="orig-alt">{orig}</td>
|
|
<td class="user-assessment">{user_ass}</td>
|
|
<td class="user-proposed">{user_prop}</td>
|
|
<td >{llm1_ass}</td>
|
|
<td >{llm1_prop}</td>
|
|
|
|
<td>
|
|
<select class="user_llm1_ass llm-select">
|
|
<option value="0" {'selected' if int(user_llm1_ass)==0 else ''}>-- none --</option>
|
|
<option value="1" {'selected' if int(user_llm1_ass)==1 else ''}>1</option>
|
|
<option value="2" {'selected' if int(user_llm1_ass)==2 else ''}>2</option>
|
|
<option value="3" {'selected' if int(user_llm1_ass)==3 else ''}>3</option>
|
|
<option value="4" {'selected' if int(user_llm1_ass)==4 else ''}>4</option>
|
|
<option value="5" {'selected' if int(user_llm1_ass)==5 else ''}>5</option>
|
|
</select>
|
|
</td>
|
|
<td >{llm2_ass}</td>
|
|
<td >{llm2_prop}</td>
|
|
<td>
|
|
<select class="user_llm2_ass llm-select">
|
|
<option value="" {'selected' if int(user_llm1_ass)==0 else ''}>-- none --</option>
|
|
<option value="1" {'selected' if int(user_llm2_ass)==1 else ''}>1</option>
|
|
<option value="2" {'selected' if int(user_llm2_ass)==2 else ''}>2</option>
|
|
<option value="3" {'selected' if int(user_llm2_ass)==3 else ''}>3</option>
|
|
<option value="4" {'selected' if int(user_llm2_ass)==4 else ''}>4</option>
|
|
<option value="5" {'selected' if int(user_llm2_ass)==5 else ''}>5</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
"""
|
|
|
|
html += """
|
|
</tbody>
|
|
</table>
|
|
"""
|
|
|
|
return gr.update(value=html), html
|
|
except Exception as e:
|
|
return f"Error rendering form: {str(e)}"
|
|
|
|
|
|
# ------- Gradio Interface -------#
|
|
|
|
|
|
# Create Gradio interface
|
|
with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
|
|
|
|
|
|
|
|
gr.HTML("""
|
|
<style>
|
|
input[type=radio]:checked {
|
|
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='black' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e") !important;
|
|
border-color: black !important;
|
|
background-color: white !important;
|
|
}
|
|
|
|
footer {
|
|
display: none !important;
|
|
}
|
|
|
|
|
|
</style>
|
|
""")
|
|
|
|
|
|
llm_response_output = gr.State()
|
|
alttext_popup_html_state = gr.State("")
|
|
user_assignment_current_status = gr.State()
|
|
process_dataframe_output_state = gr.State()
|
|
make_alttext_llm_assessment_api_call_output_state = gr.State()
|
|
|
|
with Modal(visible=False, allow_user_close=False) as alttext_modal:
|
|
gr.Markdown("## Alt Text LLMs Assessment Results")
|
|
gr.Markdown("To assess the LLMs outputs, use the dropdowns to indicate how much you agree with the LLM proposed alt text.")
|
|
alttext_modal_content = gr.HTML("")
|
|
close_modal_btn = gr.Button("Save Your Assessment", variant="secondary",elem_classes=["close-modal-btn"])
|
|
|
|
env_path = find_dotenv(filename=".env")
|
|
if env_path == "":
|
|
print("env path not found: service starting with the default params values")
|
|
_ = load_dotenv(env_path) # read .env file
|
|
db_path = return_from_env_valid("DB_PATH", "persistence/wcag_validator_ui.db")
|
|
print("db_path:", db_path)
|
|
wcag_rest_server_url = return_from_env_valid(
|
|
"WCAG_REST_SERVER_URL", "http://localhost:8000"
|
|
)
|
|
|
|
default_urls = [
|
|
"https://amazon.com",
|
|
"https://ebay.com",
|
|
]
|
|
url_list_str = return_from_env_valid("URL_LIST", json.dumps(default_urls))
|
|
url_list = json.loads(url_list_str)
|
|
|
|
print("wcag_rest_server_url:", wcag_rest_server_url)
|
|
|
|
connection_db = db_persistence_startup(
|
|
db_name_and_path=db_path, table="wcag_user_assessments"
|
|
)
|
|
print("Database connection reference available:", connection_db)
|
|
connection_db.close()
|
|
|
|
user_assignment_manager=user_assigment_management()
|
|
user_assignment_manager_state = gr.State(value=user_assignment_manager)
|
|
|
|
gr.Markdown("# WCAG AI Validator UI")
|
|
|
|
# login section
|
|
user_state = gr.State({"logged_in": False, "username": None})
|
|
with gr.Accordion(label="Your Info", open=True) as register_and_login:
|
|
with gr.Column(visible=True) as login_section:
|
|
gr.Markdown("## Login / Register")
|
|
|
|
with gr.Tab("Login"):
|
|
login_username = gr.Textbox(
|
|
label="Username", placeholder="Enter your username"
|
|
)
|
|
login_password = gr.Textbox(
|
|
label="Password", type="password", placeholder="Enter your password"
|
|
)
|
|
|
|
login_btn = gr.Button("Login", variant="primary")
|
|
login_msg = gr.Textbox(label="Login Status", interactive=False)
|
|
|
|
with gr.Tab("Register"):
|
|
reg_username = gr.Textbox(
|
|
label="Username", placeholder="Choose a username"
|
|
)
|
|
reg_password = gr.Textbox(
|
|
label="Password",
|
|
type="password",
|
|
placeholder="Choose a password (min 6 characters)",
|
|
)
|
|
reg_confirm = gr.Textbox(
|
|
label="Confirm Password",
|
|
type="password",
|
|
placeholder="Confirm your password",
|
|
)
|
|
|
|
reg_btn = gr.Button("Register", variant="primary")
|
|
reg_msg = gr.Textbox(label="Registration Status", interactive=True)
|
|
|
|
with gr.Column(visible=False) as protected_section:
|
|
|
|
content_display = gr.Textbox(
|
|
label="Your account", lines=2, interactive=False
|
|
)
|
|
|
|
logout_btn = gr.Button("Logout", variant="stop")
|
|
gr.Markdown("### Your Assignment")
|
|
user_assignment_status =gr.HTML(label="Your Assignment")
|
|
|
|
|
|
|
|
# end login section
|
|
|
|
with gr.Tab("Alt Text Assessment", visible=False) as alttext_assessment:
|
|
|
|
db_path_state = gr.State(value=db_path) # Store path in State\
|
|
wcag_rest_server_url_state = gr.State(value=wcag_rest_server_url)
|
|
with gr.Row():
|
|
with gr.Column():
|
|
|
|
with gr.Row():
|
|
with gr.Column():
|
|
url_input = gr.Dropdown(
|
|
url_list,
|
|
value=url_list[0],
|
|
multiselect=False,
|
|
label="Select an URL",
|
|
info="Select an URL to load",
|
|
)
|
|
images_number = gr.Slider(
|
|
5,
|
|
100,
|
|
value=50,
|
|
step=5,
|
|
label="Max number of images to retrieve",
|
|
visible=False,
|
|
)
|
|
with gr.Column():
|
|
|
|
image_extraction_api_call_btn = gr.Button(
|
|
"Extract Images & Alt Texts", variant="primary"
|
|
)
|
|
alttext_api_call_btn = gr.Button(
|
|
"Start LLMs Assessment",
|
|
variant="secondary",
|
|
interactive=False,
|
|
)
|
|
image_info_output = gr.Textbox(
|
|
label="Activity tracking", lines=1
|
|
)
|
|
|
|
with gr.Row(visible=False) as alttext_results_row:
|
|
|
|
# Store the DataFrame in state and render a clear HTML form for user edits
|
|
alttext_info_state = gr.State()
|
|
alttext_form = gr.HTML(label="Assessment Form")
|
|
|
|
alttext_form_data = gr.JSON(visible=False) ##gr.JSON(visible=False) because gr.State() components are not meant to receive values from JS returns
|
|
|
|
|
|
with gr.Row():
|
|
gallery_html = gr.HTML(label="Image Gallery")
|
|
|
|
|
|
|
|
image_extraction_api_call_btn.click(
|
|
fn=lambda: ("", "", gr.update(visible=False), gr.Button(interactive=False)),
|
|
inputs=[],
|
|
outputs=[
|
|
image_info_output,
|
|
gallery_html,
|
|
alttext_results_row,
|
|
alttext_api_call_btn,
|
|
],
|
|
).then(
|
|
make_image_extraction_api_call,
|
|
inputs=[url_input, images_number, wcag_rest_server_url_state,user_assignment_current_status],
|
|
outputs=[image_info_output, gallery_html],
|
|
).then(
|
|
fn=lambda: gr.Button(interactive=True),
|
|
inputs=[],
|
|
outputs=[alttext_api_call_btn],
|
|
)
|
|
|
|
alttext_api_call_btn.click(
|
|
fn=make_alttext_llm_assessment_api_call,
|
|
inputs=[
|
|
url_input,
|
|
gallery_html,
|
|
db_path_state,
|
|
wcag_rest_server_url_state,
|
|
user_state,
|
|
],
|
|
outputs=[image_info_output, alttext_info_state, llm_response_output,make_alttext_llm_assessment_api_call_output_state],
|
|
js="""
|
|
(url_input, gallery_html) => {
|
|
const checkboxes = document.querySelectorAll('.image-checkbox:checked');
|
|
if (checkboxes.length === 0) {
|
|
alert('Please select at least one image!');
|
|
return [url_input, JSON.stringify([])];
|
|
}
|
|
if (checkboxes.length > 6) {
|
|
alert('Please select maximum 6 images!');
|
|
return [url_input, JSON.stringify([])];
|
|
}
|
|
|
|
const selectedData = [];
|
|
let hasError = false; // flag to handle missing assessment
|
|
|
|
checkboxes.forEach(checkbox => {
|
|
if (hasError) return; // skip remaining iterations if error found
|
|
|
|
const index = checkbox.dataset.index;
|
|
const imageUrl = checkbox.dataset.imgurl;
|
|
const originalAlt = document.querySelector('.original-alt[data-index="' + index + '"]').value;
|
|
|
|
const assessmentInput = document.querySelector('input[name="assessment-' + index + '"]:checked');
|
|
const assessment = assessmentInput ? assessmentInput.value : null;
|
|
|
|
if (!assessment) {
|
|
alert('Please provide an assessment (1-5) for all selected images. Missing assessment for image index: ' + (parseInt(index) + 1));
|
|
hasError = true;
|
|
return; // exits forEach callback only
|
|
}
|
|
|
|
const newAltText = document.querySelector('.new-alt-text[data-index="' + index + '"]').value;
|
|
|
|
selectedData.push({
|
|
image_index: index,
|
|
image_url: imageUrl,
|
|
original_alt_text: originalAlt,
|
|
assessment: parseInt(assessment),
|
|
new_alt_text: newAltText
|
|
});
|
|
});
|
|
|
|
if (hasError) return [url_input, JSON.stringify([])]; // now actually exits outer function
|
|
|
|
return [url_input, JSON.stringify(selectedData)];
|
|
}
|
|
""",
|
|
).then(
|
|
fn=render_alttext_form,
|
|
inputs=[alttext_info_state],
|
|
outputs=[alttext_form,alttext_popup_html_state],
|
|
).then(fn=maybe_open_modal,#open modal
|
|
inputs=[make_alttext_llm_assessment_api_call_output_state], # gr.State that holds your condition
|
|
outputs=[alttext_modal]
|
|
).then(
|
|
fn=lambda html: (gr.update(value=html)),
|
|
inputs=[alttext_popup_html_state],
|
|
outputs=[alttext_modal_content], # ← populate modal
|
|
)
|
|
|
|
|
|
|
|
|
|
close_modal_btn.click( #the close button now save
|
|
fn=process_dataframe,
|
|
inputs=[db_path_state, url_input, alttext_form_data, user_state,llm_response_output],
|
|
outputs=[image_info_output,process_dataframe_output_state],
|
|
js="""
|
|
(db_path_state, url_input, alttext_form_data, user_state, llm_response_output) => {
|
|
|
|
const rows = document.querySelectorAll('.alttext-row');
|
|
const selectedData = [];
|
|
|
|
// Check all rows first — if any select is unset (0 or empty), return empty list
|
|
const hasUnset = Array.from(rows).some(row => {
|
|
const user_llm1_ass = parseInt(row.querySelector('.user_llm1_ass')?.value || '0');
|
|
const user_llm2_ass = parseInt(row.querySelector('.user_llm2_ass')?.value || '0');
|
|
return user_llm1_ass === 0 || user_llm2_ass === 0;
|
|
});
|
|
console.log("hasUnset:",hasUnset)
|
|
|
|
if (hasUnset)
|
|
{alert('Please provide an assessment (1-5) for all selected images for both models');
|
|
return [db_path_state, url_input, [], user_state, llm_response_output];}
|
|
|
|
rows.forEach(row => {
|
|
const imgNum = row.querySelector('.img-num')?.innerText || '';
|
|
const origAlt = row.querySelector('.orig-alt')?.innerText || '';
|
|
const userAssessment = row.querySelector('.user-assessment')?.innerText || '3';
|
|
const userProposed = row.querySelector('.user-proposed')?.innerText || '';
|
|
const user_llm1_ass = row.querySelector('.user_llm1_ass')?.value || '0';
|
|
const user_llm2_ass = row.querySelector('.user_llm2_ass')?.value || '0';
|
|
|
|
selectedData.push({
|
|
"Image #": imgNum,
|
|
"Original Alt Text": origAlt,
|
|
"User Assessment": parseInt(userAssessment) || 3,
|
|
"User Proposed Alt Text": userProposed,
|
|
"User Assessment for LLM Proposal 1": parseInt(user_llm1_ass),
|
|
"User Assessment for LLM Proposal 2": parseInt(user_llm2_ass)
|
|
});
|
|
});
|
|
console.log("selectedData:",selectedData);
|
|
return [db_path_state, url_input, selectedData, user_state, llm_response_output];
|
|
}
|
|
""",
|
|
).then( # Close button dismisses the modal
|
|
fn=maybe_close_modal,
|
|
inputs=[process_dataframe_output_state], # gr.State that holds your condition
|
|
outputs=[alttext_modal],
|
|
js="""
|
|
async (is_valid) => {
|
|
console.log("is_valid animation:",is_valid) // da sistemare, animazione non gestita
|
|
if (!is_valid) {
|
|
console.log("skip animation");
|
|
return;} // Skip animation if not closing
|
|
|
|
const btn = document.querySelector('.close-modal-btn');
|
|
|
|
// Change button text
|
|
btn.textContent = 'Saving...';
|
|
|
|
// Fade out
|
|
const modal = document.querySelector('.modal-container');
|
|
modal.style.transition = 'opacity 0.4s ease';
|
|
modal.style.opacity = '0';
|
|
|
|
// Wait for fade
|
|
await new Promise(resolve => setTimeout(resolve, 400));
|
|
}
|
|
"""
|
|
|
|
|
|
).then(# refresh the user assignment display after saving the assessment
|
|
fn=display_user_assignment, inputs=[db_path_state,user_state], outputs=[user_assignment_current_status]).then(fn=render_user_assessmnet_status_table, inputs=[user_assignment_current_status], outputs=[user_assignment_status]) # sync the user assignment display after saving the assessment
|
|
|
|
# placed here at the end to give full contents visibility to events
|
|
# Event handlers
|
|
login_btn.click(
|
|
fn=login_user,
|
|
inputs=[login_username, login_password, user_state],
|
|
outputs=[
|
|
login_msg,
|
|
reg_msg,
|
|
user_state,
|
|
login_section,
|
|
protected_section,
|
|
alttext_assessment,
|
|
register_and_login,
|
|
],
|
|
).then(fn=protected_content, inputs=[user_state], outputs=[content_display]).then(fn=display_user_assignment, inputs=[db_path_state,user_state], outputs=[user_assignment_current_status]).then(fn=render_user_assessmnet_status_table, inputs=[user_assignment_current_status], outputs=[user_assignment_status]) # display the user assignment after login
|
|
|
|
reg_btn.click(
|
|
fn=register_user,
|
|
inputs=[reg_username, reg_password, reg_confirm,user_assignment_manager_state],
|
|
outputs=[login_msg, reg_msg, user_state],
|
|
)
|
|
|
|
logout_btn.click(
|
|
fn=logout_user,
|
|
inputs=[user_state],
|
|
outputs=[
|
|
login_msg,
|
|
user_state,
|
|
login_section,
|
|
protected_section,
|
|
alttext_assessment,
|
|
],
|
|
).then(
|
|
fn=lambda: ("", "", gr.update(visible=False), gr.Button(interactive=False)),
|
|
inputs=[],
|
|
outputs=[
|
|
image_info_output,
|
|
gallery_html,
|
|
alttext_results_row,
|
|
alttext_api_call_btn,
|
|
],
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
demo.launch(server_name="0.0.0.0", server_port=7860)
|