#### 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 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 = user_assignment_manager.get_all_user_ids() print(f"Currently managed users from db: {managed_users}") print(f"Total managed users from db: {user_assignment_manager.get_managed_user_count()}\n") user_assignment_stats = user_assignment_manager.get_statistics() print(f"Current assignment stats:{user_assignment_stats} \n") WCAG_VALIDATOR_RESTSERVER_HEADERS = [("Content-Type", "application/json")] def display_user_assignment(user_state): if user_state and "username" in user_state: username = user_state["username"] 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 List": assignments[url] } ) 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)) # 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)}" 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" except KeyError: return f"No data Saved because no image selected. Please select at least one image." except Exception as e: return f"Error processing User Assessment for LLM Proposal: {str(e)}" 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" 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, # to improve 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() return "User assessment saved successfully!" def load_images_from_json(json_input): """Extract URLs and alt text from JSON and create HTML gallery""" 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 = """ " 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" return "LLM assessment not started", pd.DataFrame(), {} # 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(3) # default value for now user_assessments_llm_proposal_2.append(3) # 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 def make_image_extraction_api_call( url, number_of_images=30, wcag_rest_server_url="http://localhost:8000", ): 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) 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: return "" 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 = """ """ 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", 3) user_llm2_ass = row.get("User Assessment for LLM Proposal 2", 3) html += f""" """ html += """
Image # Original Alt Text User Assessment User Proposed Alt Text LLM Assessment 1 LLM Proposed Alt Text 1 User Assessment for LLM Proposal 1 LLM Assessment 2 LLM Proposed Alt Text 2 User Assessment for LLM Proposal 2
{imgnum} {orig} {user_ass} {user_prop} {llm1_ass} {llm1_prop} {llm2_ass} {llm2_prop}
""" 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(""" """) llm_response_output = gr.State() alttext_popup_html_state = gr.State("") user_assignment_manager_state = gr.State(value=user_assignment_manager) 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() 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=5, interactive=False ) user_assignment_status = gr.DataFrame( headers=[ "Website URL", "Assigned Image Number List" #"Assignment Status", ], label="Your Current Assignment", wrap=True, # Wrap text in cells interactive=False, scale=7, ) logout_btn = gr.Button("Logout", variant="stop") # 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) 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], 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], 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 > 3) { alert('Please select maximum 3 images!'); return [url_input,JSON.stringify([])]; } const selectedData = []; checkboxes.forEach(checkbox => { const index = checkbox.dataset.index; const imageUrl = checkbox.dataset.imgurl; const originalAlt = document.querySelector('.original-alt[data-index="' + index + '"]').value; const assessment = document.querySelector('input[name="assessment-' + index + '"]:checked').value; 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 }); }); return [url_input,JSON.stringify(selectedData)]; } """, ).then( fn=render_alttext_form, inputs=[alttext_info_state], outputs=[alttext_form,alttext_popup_html_state], ).then( fn=lambda html: (gr.update(value=html), Modal(visible=True)), inputs=[alttext_popup_html_state], outputs=[alttext_modal_content, alttext_modal], # ← populate + open 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], js=""" (db_path_state, url_input, alttext_form_html, user_state, llm_response_output) => { const rows = document.querySelectorAll('.alttext-row'); const selectedData = []; 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 || '3'; const user_llm2_ass = row.querySelector('.user_llm2_ass')?.value || '3'; 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) }); }); return [db_path_state, url_input, selectedData, user_state, llm_response_output]; } """, ).then( # Close button dismisses the modal fn=lambda: Modal(visible=False), inputs=[], outputs=[alttext_modal], js=""" async () => { 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)); } """ ) # 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=[user_state], outputs=[user_assignment_status]) 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, ], ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)