new UI with multiple models

This commit is contained in:
Nicola Leonardi 2026-03-04 16:07:09 +01:00
parent 3d0936448b
commit eca78a5ae2
9 changed files with 1859 additions and 69 deletions

5
UI/README.md Normal file
View File

@ -0,0 +1,5 @@
# Versions
- wcag_validator_ui_pre_multimodel.py : the version used for the 12_2025 user test. Has to work with restServer with only one model (not two)
- wcag_validator_ui.py : the official version

View File

@ -1,4 +1,5 @@
gradio==5.49.1
pandas==2.3.3
python-dotenv==1.2.1
requests==2.32.5
requests==2.32.5
gradio-modal==0.0.4

View File

@ -3,6 +3,7 @@
# python wcag_validator_ui.py
import gradio as gr
from gradio_modal import Modal
import requests
from pathlib import Path
import sys
@ -33,7 +34,19 @@ WCAG_VALIDATOR_RESTSERVER_HEADERS = [("Content-Type", "application/json")]
def process_dataframe(db_path, url, updated_df, user_state={},llm_response_output={}):
print("Processing dataframe to adjust columns...")
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
@ -41,6 +54,10 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu
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
@ -50,7 +67,8 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu
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)
lm_response_output_str = json.dumps(llm_response_output, ensure_ascii=False) #recuperato dalla chiamata all'llm, ho tutte le info anche sulle immagini
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(
@ -59,7 +77,7 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu
page_url=url,
user=json_user_str,
llm_model="",
json_in_str=lm_response_output_str,#dataframe_json, # to improve
json_in_str=llm_response_output_str,#dataframe_json, # to improve
json_out_str=dataframe_json,
table="wcag_user_assessments",
)
@ -81,7 +99,7 @@ def load_images_from_json(json_input):
images = data["images"]
info_text = f"Found {len(images)} image(s)"
print(f"Found {len(data['images'])} image(s)")
# Create HTML gallery with checkboxes and assessment forms
html = """
@ -133,7 +151,7 @@ def load_images_from_json(json_input):
display: none;
margin-top: 15px;
padding: 10px;
background: #7896b9;
background: aliceblue;
border-radius: 4px;
border: 1px solid #2196F3;
}
@ -376,8 +394,8 @@ def make_alttext_llm_assessment_api_call(
if not selected_images or len(selected_images) == 0:
info_text = "No images selected"
print(info_text)
return "LLM assessment not started", pd.DataFrame()
return "LLM assessment not started", pd.DataFrame(), {}
# prepare data for insertion
json_in_str = {}
@ -442,7 +460,7 @@ def make_alttext_llm_assessment_api_call(
info_dataframe["User Assessment for LLM Proposal 2"] = (
user_assessments_llm_proposal_2
)
print("info_dataframe after adding user assessments:", info_dataframe)
#print("info_dataframe after adding user assessments:", info_dataframe)
except Exception as e:
return {"error": str(e)}
@ -493,12 +511,134 @@ def make_image_extraction_api_call(
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 = """
<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", 3)
user_llm2_ass = row.get("User Assessment for LLM Proposal 2", 3)
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="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="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("")
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")
@ -539,6 +679,7 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
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)
@ -556,8 +697,9 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
type="password",
placeholder="Confirm your password",
)
reg_btn = gr.Button("Register", variant="primary")
reg_msg = gr.Textbox(label="Registration Status", interactive=False)
reg_msg = gr.Textbox(label="Registration Status", interactive=True)
with gr.Column(visible=False) as protected_section:
@ -598,7 +740,7 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
"Extract Images & Alt Texts", variant="primary"
)
alttext_api_call_btn = gr.Button(
"Start LLM Assessment",
"Start LLMs Assessment",
variant="secondary",
interactive=False,
)
@ -608,41 +750,16 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
with gr.Row(visible=False) as alttext_results_row:
# Use DataFrame for tabular output
alttext_info_output = gr.DataFrame(
headers=[
"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",
],
label="LLM Assessment Results",
wrap=True, # Wrap text in cells
interactive=True,
scale=7,
)
with gr.Column():
save_user_assessment_btn = gr.Button(
"Save Your Assessment",
variant="secondary",
interactive=True,
scale=1,
)
gr.Markdown(
" Info: to assess the LLM output, only the values for the 'User Assessment for LLM Proposal' column need to be changed."
)
llm_response_output=gr.JSON()
# 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)),
@ -663,8 +780,6 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
outputs=[alttext_api_call_btn],
)
# Process selected images with JavaScript
alttext_api_call_btn.click(
fn=make_alttext_llm_assessment_api_call,
inputs=[
@ -674,7 +789,7 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
wcag_rest_server_url_state,
user_state,
],
outputs=[image_info_output, alttext_info_output,llm_response_output],
outputs=[image_info_output, alttext_info_state, llm_response_output],
js="""
(url_input,gallery_html) => {
const checkboxes = document.querySelectorAll('.image-checkbox:checked');
@ -693,7 +808,6 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
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;
console.log("assessment:",assessment)
const newAltText = document.querySelector('.new-alt-text[data-index="' + index + '"]').value;
selectedData.push({
@ -709,15 +823,65 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
}
""",
).then(
fn=lambda: gr.update(visible=True),
inputs=[],
outputs=[alttext_results_row],
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
)
save_user_assessment_btn.click(
close_modal_btn.click( #the close button now save
fn=process_dataframe,
inputs=[db_path_state, url_input, alttext_info_output, user_state,llm_response_output],
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

View File

@ -0,0 +1,686 @@
#### To launch the script
# gradio wcag_validator_ui.py
# python wcag_validator_ui.py
# this is the UI version for the 12_2025 student test. The rest Server should work on single model
import gradio as gr
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
WCAG_VALIDATOR_RESTSERVER_HEADERS = [("Content-Type", "application/json")]
def process_dataframe(db_path, url, updated_df, user_state={}):
print("Processing dataframe to adjust columns...")
column_rating_name = "User Assessment for LLM Proposal"
# 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"
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)
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=dataframe_json, # to improve
json_out_str="done via UI",
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)"
print(f"Found {len(data['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: #7896b9;
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")
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 > 3) {{
this.checked = false;
alert('Maximum 3 images can be selected!');
return;
}}
panel.classList.add('visible');
}} else {{
panel.classList.remove('visible');
}}
">
Select #{idx + 1}
</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}" checked>
<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()
info_text = f"Assessment done on {len(data['mllm_validations']['mllm_alttext_assessments'])} image(s)\n\n"
print(
f"Assessment done on {len(data['mllm_validations']['mllm_alttext_assessments'])} image(s)"
)
data_frame = []
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,
}
)
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(info_text)
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 = []
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.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)
# 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"] = (
user_assessments_llm_proposal
)
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
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)}
# ------- Gradio Interface -------#
# Create Gradio interface
with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
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="Register & Login", 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=False)
with gr.Column(visible=False) as protected_section:
content_display = gr.Textbox(
label="Your account", lines=5, interactive=False
)
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 in iframe",
)
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 LLM Assessment",
variant="secondary",
interactive=False,
)
image_info_output = gr.Textbox(
label="Activity tracking", lines=1
)
with gr.Row(visible=False) as alttext_results_row:
# Use DataFrame for tabular output
alttext_info_output = gr.DataFrame(
headers=[
"Image #",
"Original Alt Text",
"User Assessment",
"User Proposed Alt Text",
"LLM Assessment",
"LLM Proposed Alt Text",
"User Assessment for LLM Proposal",
],
label="LLM Assessment Results",
wrap=True, # Wrap text in cells
interactive=True,
scale=7,
)
with gr.Column():
save_user_assessment_btn = gr.Button(
"Save Your Assessment",
variant="secondary",
interactive=True,
scale=1,
)
gr.Markdown(
" Info: to assess the LLM output, only the values for the 'User Assessment for LLM Proposal' column need to be changed."
)
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],
)
# Process selected images with JavaScript
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_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;
console.log("assessment:",assessment)
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=lambda: gr.update(visible=True),
inputs=[],
outputs=[alttext_results_row],
)
save_user_assessment_btn.click(
fn=process_dataframe,
inputs=[db_path_state, url_input, alttext_info_output, user_state],
outputs=[image_info_output],
)
# 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])
reg_btn.click(
fn=register_user,
inputs=[reg_username, reg_password, reg_confirm],
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)

View File

@ -445,7 +445,7 @@ class MLLMManager:
### Other utility functions
def parse_mllm_alt_text_response(mllm_response):
def parse_mllm_alt_text_response(mllm_response, model_id=""):
"""
Parse an MLLM response string and extract key attributes into a JSON object.
@ -467,6 +467,7 @@ def parse_mllm_alt_text_response(mllm_response):
"assessment": None,
"evaluation_result": None,
"new_alt_text": None,
"mllm_model": model_id
}
# Extract JSON content between ```json and ``` markers
@ -482,6 +483,7 @@ def parse_mllm_alt_text_response(mllm_response):
"assessment": None,
"evaluation_result": None,
"new_alt_text": None,
"mllm_model": model_id
}
json_str = (
@ -499,6 +501,7 @@ def parse_mllm_alt_text_response(mllm_response):
"assessment": parsed_data.get("Assessment", ""),
"evaluation_result": parsed_data.get("EvaluationResult", ""),
"new_alt_text": parsed_data.get("New alt-text", ""),
"mllm_model": model_id
}
return result
@ -510,6 +513,7 @@ def parse_mllm_alt_text_response(mllm_response):
"assessment": None,
"evaluation_result": None,
"new_alt_text": None,
"mllm_model": model_id
}
except Exception as e:
print(f"Error parsing MLLM response: {e}")
@ -518,10 +522,11 @@ def parse_mllm_alt_text_response(mllm_response):
"assessment": None,
"evaluation_result": None,
"new_alt_text": None,
"mllm_model": model_id
}
def parse_mllm_standard_response(mllm_response, extra_fields=[]):
def parse_mllm_standard_response(mllm_response, extra_fields=[],model_id=""):
try:
# Handle NaN or None values
@ -530,6 +535,7 @@ def parse_mllm_standard_response(mllm_response, extra_fields=[]):
"assessment": None,
"judgment": None,
"evaluation_result": None,
"mllm_model": model_id
}
# Extract JSON content between ```json and ``` markers
# json_match = re.search(r"```json\s*(.*?)\s*```", mllm_response, re.DOTALL)
@ -560,6 +566,7 @@ def parse_mllm_standard_response(mllm_response, extra_fields=[]):
"assessment": None,
"judgment": None,
"evaluation_result": None,
"mllm_model": model_id
}
json_str = (
@ -580,6 +587,7 @@ def parse_mllm_standard_response(mllm_response, extra_fields=[]):
"assessment": parsed_data.get("Assessment", ""),
"judgment": parsed_data.get("Judgment", ""),
"evaluation_result": parsed_data.get("EvaluationResult", ""),
"mllm_model": model_id
}
if extra_fields:
for field in extra_fields:
@ -590,6 +598,7 @@ def parse_mllm_standard_response(mllm_response, extra_fields=[]):
"assessment": None,
"judgment": None,
"evaluation_result": None,
"mllm_model": model_id
}
elif isinstance(
parsed_data, list
@ -601,6 +610,7 @@ def parse_mllm_standard_response(mllm_response, extra_fields=[]):
"assessment": item.get("Assessment", ""),
"judgment": item.get("Judgment", ""),
"evaluation_result": item.get("EvaluationResult", ""),
"mllm_model": model_id
}
if extra_fields:
for field in extra_fields:
@ -611,6 +621,7 @@ def parse_mllm_standard_response(mllm_response, extra_fields=[]):
"assessment": None,
"judgment": None,
"evaluation_result": None,
"mllm_model": model_id
}
result.append(item_result)
return result
@ -621,6 +632,7 @@ def parse_mllm_standard_response(mllm_response, extra_fields=[]):
"assessment": None,
"judgment": None,
"evaluation_result": None,
"mllm_model": model_id
}
except Exception as e:
print(f"Error parsing MLLM response: {e}")
@ -628,4 +640,5 @@ def parse_mllm_standard_response(mllm_response, extra_fields=[]):
"assessment": None,
"judgment": None,
"evaluation_result": None,
"mllm_model": model_id
}

View File

@ -103,21 +103,29 @@ class WCAGAltTextValuationRoutes:
extract_context=True,
)
if self.mllm_settings["openai_model"] == "Both":
from concurrent.futures import ThreadPoolExecutor
def run_model_evaluation(endpoint, api_key, model_id, openai_model, label):
def run_model_evaluation(
endpoint, api_key, model_id, openai_model, label
):
manager = MLLMManager(endpoint, api_key, model_id)
print(f"Using {label} model for alt text evaluation.", manager.end_point)
print(
f"Using {label} model for alt text evaluation.",
manager.end_point,
)
logging.info("mllm_end_point:%s", endpoint)
logging.info("mllm_model_id:%s", model_id)
responses = manager.make_alt_text_evaluation(images, openai_model=openai_model)
responses = manager.make_alt_text_evaluation(
images, openai_model=openai_model
)
for i, response in enumerate(responses):
responses[i]["mllm_response"] = parse_mllm_alt_text_response(response["mllm_response"])
responses[i]["mllm_response"] = parse_mllm_alt_text_response(
response["mllm_response"], model_id=model_id
)
return responses
@ -127,25 +135,27 @@ class WCAGAltTextValuationRoutes:
self.mllm_settings["mllm_end_point"]["model_end_point_remote"],
self.mllm_settings["mllm_api_key"]["api_key_remote"],
self.mllm_settings["mllm_model_id"]["model_id_remote"],
True, "first remote"
True,
"first remote",
)
future_local = executor.submit(
run_model_evaluation,
self.mllm_settings["mllm_end_point"]["model_end_point_local"],
self.mllm_settings["mllm_api_key"]["api_key_local"],
self.mllm_settings["mllm_model_id"]["model_id_local"],
False, "second local"
False,
"second local",
)
mllm_responses_openai = future_openai.result()
mllm_responses_local = future_local.result()
mllm_responses_local = future_local.result()
mllm_responses_object = {
"mllm_alttext_assessments": {
"mllm_alttext_assessments_openai": mllm_responses_openai,
"mllm_alttext_assessments_local": mllm_responses_local,
}
}
}
else:
# MLLM settings
@ -170,7 +180,7 @@ class WCAGAltTextValuationRoutes:
# Parse MLLM responses
for i, response in enumerate(mllm_responses):
parsed_resp = parse_mllm_alt_text_response(
response["mllm_response"]
response["mllm_response"], model_id=mllm_model_id
)
mllm_responses[i]["mllm_response"] = parsed_resp

View File

@ -103,8 +103,9 @@ class WCAG_g88ValuationRoutes:
)
parsed_mllm_responses = parse_mllm_standard_response(
responses["mllm_response"]
responses["mllm_response"], model_id=model_id
)
return parsed_mllm_responses
with ThreadPoolExecutor(max_workers=2) as executor:
@ -156,8 +157,9 @@ class WCAG_g88ValuationRoutes:
openai_model=self.mllm_settings["openai_model"],
)
parsed_mllm_responses = parse_mllm_standard_response(
mllm_responses["mllm_response"]
mllm_responses["mllm_response"], model_id=mllm_model_id
)
mllm_responses_object = {"mllm_g88_assessments": parsed_mllm_responses}
# common: prepare the object to return in the response

View File

@ -120,9 +120,9 @@ class WCAG_h58ValuationRoutes:
"html",
"detected_lang",
"declared_lang",
],
],model_id=model_id
)
mllm_respones_flattened.extend(parsed_resp)
return mllm_respones_flattened
@ -186,7 +186,9 @@ class WCAG_h58ValuationRoutes:
parsed_resp = parse_mllm_standard_response(
response["mllm_response"],
extra_fields=["tag", "html", "detected_lang", "declared_lang"],
model_id=mllm_model_id
)
mllm_respones_flattened.extend(parsed_resp)