Spaces:
Sleeping
Sleeping
Prince Vaviya commited on
Commit ·
7a83d16
1
Parent(s): 76c0be6
size_predictor
Browse files- app.py +139 -0
- feedback.csv +9 -0
- requirements.txt +1 -0
- size_chart.py +32 -0
- size_rules.py +87 -0
app.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import csv
|
| 3 |
+
import os
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from size_rules import evaluate_size, apply_fit_preference
|
| 6 |
+
|
| 7 |
+
FEEDBACK_FILE = "feedback.csv"
|
| 8 |
+
|
| 9 |
+
# Initialize CSV if it doesn't exist
|
| 10 |
+
if not os.path.exists(FEEDBACK_FILE):
|
| 11 |
+
with open(FEEDBACK_FILE, "w", newline="") as f:
|
| 12 |
+
writer = csv.writer(f)
|
| 13 |
+
writer.writerow(["Timestamp", "Chest", "Waist", "Bicep", "Shoulder", "Fit_Pref", "Base_Size", "Predicted_Size", "Feedback"])
|
| 14 |
+
|
| 15 |
+
def predict_size(chest, waist, bicep, shoulder, fit_pref):
|
| 16 |
+
data = {
|
| 17 |
+
"chest": chest,
|
| 18 |
+
"waist": waist,
|
| 19 |
+
"bicep": bicep,
|
| 20 |
+
"shoulder": shoulder
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
base_size, base_reason = evaluate_size(data)
|
| 24 |
+
final_size, fit_reason = apply_fit_preference(base_size, fit_pref, data)
|
| 25 |
+
|
| 26 |
+
explanation = {
|
| 27 |
+
"recommended_size": final_size,
|
| 28 |
+
"base_size": base_size,
|
| 29 |
+
"fit_preference": fit_pref,
|
| 30 |
+
"final_size": final_size,
|
| 31 |
+
"confidence": "high",
|
| 32 |
+
"reason": f"{base_reason}. {fit_reason}"
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
html_output = f"""
|
| 36 |
+
<div style="
|
| 37 |
+
background-color: #d1fae5;
|
| 38 |
+
color: #065f46;
|
| 39 |
+
font-size: 32px;
|
| 40 |
+
font-weight: bold;
|
| 41 |
+
text-align: center;
|
| 42 |
+
border: 2px solid #10b981;
|
| 43 |
+
padding: 20px;
|
| 44 |
+
border-radius: 8px;
|
| 45 |
+
">
|
| 46 |
+
{final_size}
|
| 47 |
+
</div>
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
# Return data for state
|
| 51 |
+
state_data = {
|
| 52 |
+
"chest": chest,
|
| 53 |
+
"waist": waist,
|
| 54 |
+
"bicep": bicep,
|
| 55 |
+
"shoulder": shoulder,
|
| 56 |
+
"fit": fit_pref,
|
| 57 |
+
"base": base_size,
|
| 58 |
+
"prediction": final_size
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
return html_output, explanation, state_data
|
| 62 |
+
|
| 63 |
+
def save_feedback(vote, data):
|
| 64 |
+
if not data:
|
| 65 |
+
return "No prediction data found."
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
with open(FEEDBACK_FILE, "a", newline="") as f:
|
| 69 |
+
writer = csv.writer(f)
|
| 70 |
+
writer.writerow([
|
| 71 |
+
datetime.now().isoformat(),
|
| 72 |
+
data["chest"],
|
| 73 |
+
data["waist"],
|
| 74 |
+
data["bicep"],
|
| 75 |
+
data["shoulder"],
|
| 76 |
+
data["fit"],
|
| 77 |
+
data["base"],
|
| 78 |
+
data["prediction"],
|
| 79 |
+
vote
|
| 80 |
+
])
|
| 81 |
+
return f"Thanks! for the feedback"
|
| 82 |
+
except Exception as e:
|
| 83 |
+
return f"Error saving feedback: {str(e)}"
|
| 84 |
+
|
| 85 |
+
css = """
|
| 86 |
+
#result-box textarea {
|
| 87 |
+
font-size: 32px !important;
|
| 88 |
+
}
|
| 89 |
+
"""
|
| 90 |
+
|
| 91 |
+
with gr.Blocks(title="AI Size Recommendation Engine") as demo:
|
| 92 |
+
gr.Markdown("# AI Size Recommendation Engine")
|
| 93 |
+
gr.Markdown("Enter your body measurements (in inches) to get a deterministic size recommendation.")
|
| 94 |
+
|
| 95 |
+
# State to store the last prediction data
|
| 96 |
+
prediction_state = gr.State()
|
| 97 |
+
|
| 98 |
+
with gr.Row():
|
| 99 |
+
with gr.Column():
|
| 100 |
+
chest = gr.Number(label="Chest (inches)", value=38.0, step=0.5)
|
| 101 |
+
waist = gr.Number(label="Waist (inches)", value=32.0, step=0.5)
|
| 102 |
+
bicep = gr.Number(label="Bicep (inches)", value=13.0, step=0.5)
|
| 103 |
+
shoulder = gr.Number(label="Shoulder (inches)", value=46.0, step=0.5)
|
| 104 |
+
fit = gr.Dropdown(choices=["tight", "regular", "loose"], label="Fit Preference", value="regular")
|
| 105 |
+
submit_btn = gr.Button("Predict Size", variant="primary")
|
| 106 |
+
|
| 107 |
+
with gr.Column():
|
| 108 |
+
gr.Markdown("### Recommended Size")
|
| 109 |
+
output_size = gr.HTML()
|
| 110 |
+
output_json = gr.JSON(label="Explainable AI Output")
|
| 111 |
+
|
| 112 |
+
with gr.Row():
|
| 113 |
+
like_btn = gr.Button("👍 Good Prediction")
|
| 114 |
+
dislike_btn = gr.Button("👎 Bad Prediction")
|
| 115 |
+
|
| 116 |
+
feedback_msg = gr.Label(label="Feedback Status", visible=False)
|
| 117 |
+
|
| 118 |
+
# Prediction event
|
| 119 |
+
submit_btn.click(
|
| 120 |
+
fn=predict_size,
|
| 121 |
+
inputs=[chest, waist, bicep, shoulder, fit],
|
| 122 |
+
outputs=[output_size, output_json, prediction_state]
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Feedback events
|
| 126 |
+
like_btn.click(
|
| 127 |
+
fn=lambda d: save_feedback("Like", d),
|
| 128 |
+
inputs=[prediction_state],
|
| 129 |
+
outputs=[feedback_msg]
|
| 130 |
+
).then(lambda: gr.Label(visible=True), None, feedback_msg)
|
| 131 |
+
|
| 132 |
+
dislike_btn.click(
|
| 133 |
+
fn=lambda d: save_feedback("Dislike", d),
|
| 134 |
+
inputs=[prediction_state],
|
| 135 |
+
outputs=[feedback_msg]
|
| 136 |
+
).then(lambda: gr.Label(visible=True), None, feedback_msg)
|
| 137 |
+
|
| 138 |
+
if __name__ == "__main__":
|
| 139 |
+
demo.launch()
|
feedback.csv
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Timestamp,Chest,Waist,Bicep,Shoulder,Fit_Pref,Base_Size,Predicted_Size,Feedback
|
| 2 |
+
2026-01-02T15:26:54.295743,38,32,13,46,regular,S,M,Like
|
| 3 |
+
2026-01-02T15:26:58.463759,38,32,13,46,regular,S,M,Dislike
|
| 4 |
+
2026-01-02T15:28:21.262074,38,32,13,46,regular,S,M,Like
|
| 5 |
+
2026-01-02T15:28:42.327924,38,32,13,46,regular,S,M,Like
|
| 6 |
+
2026-01-02T15:28:43.110742,38,32,13,46,regular,S,M,Dislike
|
| 7 |
+
2026-01-02T15:28:44.095717,38,32,13,46,regular,S,M,Dislike
|
| 8 |
+
2026-01-02T15:28:44.377499,38,32,13,46,regular,S,M,Dislike
|
| 9 |
+
2026-01-02T15:28:45.495196,38,32,13,46,regular,S,M,Dislike
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
gradio
|
size_chart.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SIZE_CHART = {
|
| 2 |
+
"XS": {
|
| 3 |
+
"chest": (34, 36),
|
| 4 |
+
"waist": (28, 30),
|
| 5 |
+
"bicep": (11, 12),
|
| 6 |
+
"shoulder": (42, 44)
|
| 7 |
+
},
|
| 8 |
+
"S": {
|
| 9 |
+
"chest": (36, 38),
|
| 10 |
+
"waist": (30, 32),
|
| 11 |
+
"bicep": (12, 13),
|
| 12 |
+
"shoulder": (44, 46)
|
| 13 |
+
},
|
| 14 |
+
"M": {
|
| 15 |
+
"chest": (38, 40),
|
| 16 |
+
"waist": (32, 34),
|
| 17 |
+
"bicep": (13, 14),
|
| 18 |
+
"shoulder": (46, 48)
|
| 19 |
+
},
|
| 20 |
+
"L": {
|
| 21 |
+
"chest": (40, 42),
|
| 22 |
+
"waist": (34, 36),
|
| 23 |
+
"bicep": (14, 15),
|
| 24 |
+
"shoulder": (48, 50)
|
| 25 |
+
},
|
| 26 |
+
"XL": {
|
| 27 |
+
"chest": (42, 44),
|
| 28 |
+
"waist": (36, 38),
|
| 29 |
+
"bicep": (15, 16),
|
| 30 |
+
"shoulder": (50, 52)
|
| 31 |
+
}
|
| 32 |
+
}
|
size_rules.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from size_chart import SIZE_CHART
|
| 2 |
+
|
| 3 |
+
PRIORITY = ["shoulder", "chest", "waist", "bicep"]
|
| 4 |
+
|
| 5 |
+
def evaluate_size(measurements):
|
| 6 |
+
"""
|
| 7 |
+
Evaluates the base size based on measurements.
|
| 8 |
+
Returns: (size, reason)
|
| 9 |
+
"""
|
| 10 |
+
ordered_sizes = ["XS", "S", "M", "L", "XL"]
|
| 11 |
+
|
| 12 |
+
for i, size in enumerate(ordered_sizes):
|
| 13 |
+
limits = SIZE_CHART[size]
|
| 14 |
+
valid = True
|
| 15 |
+
|
| 16 |
+
# Check if measurements fit within this size's limits
|
| 17 |
+
for key in PRIORITY:
|
| 18 |
+
# We strictly check the upper bound.
|
| 19 |
+
# If measurement > max, this size is too small.
|
| 20 |
+
if measurements[key] > limits[key][1]:
|
| 21 |
+
valid = False
|
| 22 |
+
break
|
| 23 |
+
|
| 24 |
+
if valid:
|
| 25 |
+
# This is the smallest size that fits.
|
| 26 |
+
# Determine reasoning.
|
| 27 |
+
if i == 0:
|
| 28 |
+
return size, "Smallest available size fits all measurements."
|
| 29 |
+
|
| 30 |
+
# Check why the previous size failed logic
|
| 31 |
+
prev_size = ordered_sizes[i-1]
|
| 32 |
+
prev_limits = SIZE_CHART[prev_size]
|
| 33 |
+
reasons = []
|
| 34 |
+
for key in PRIORITY:
|
| 35 |
+
if measurements[key] > prev_limits[key][1]:
|
| 36 |
+
reasons.append(f"{key.capitalize()} ({measurements[key]}) > {prev_size} limit ({prev_limits[key][1]})")
|
| 37 |
+
|
| 38 |
+
reason_str = f"Requires {size} because: " + ", ".join(reasons)
|
| 39 |
+
return size, reason_str
|
| 40 |
+
|
| 41 |
+
# If even XL fails (XL max is exceeded)
|
| 42 |
+
# We return XL+
|
| 43 |
+
# Find what exceeded XL
|
| 44 |
+
xl_limits = SIZE_CHART["XL"]
|
| 45 |
+
reasons = []
|
| 46 |
+
for key in PRIORITY:
|
| 47 |
+
if measurements[key] > xl_limits[key][1]:
|
| 48 |
+
reasons.append(f"{key.capitalize()} ({measurements[key]}) > XL limit ({xl_limits[key][1]})")
|
| 49 |
+
|
| 50 |
+
return "XL+", "Measurements exceed XL limits: " + ", ".join(reasons)
|
| 51 |
+
|
| 52 |
+
def apply_fit_preference(base_size, preference, measurements=None):
|
| 53 |
+
"""
|
| 54 |
+
Applies fit preference to the base size.
|
| 55 |
+
For 'regular', requires measurements to re-evaluate.
|
| 56 |
+
Returns: (final_size, reason)
|
| 57 |
+
"""
|
| 58 |
+
sizes = ["XS", "S", "M", "L", "XL"]
|
| 59 |
+
|
| 60 |
+
if base_size not in sizes:
|
| 61 |
+
return base_size, "Fit preference not applicable for sizes outside chart"
|
| 62 |
+
|
| 63 |
+
idx = sizes.index(base_size)
|
| 64 |
+
|
| 65 |
+
if preference == "tight":
|
| 66 |
+
return base_size, "Fit: Tight (Kept base size)"
|
| 67 |
+
|
| 68 |
+
if preference == "loose":
|
| 69 |
+
if idx < len(sizes) - 1:
|
| 70 |
+
return sizes[idx + 1], "Fit: Loose (Moved up one size)"
|
| 71 |
+
else:
|
| 72 |
+
return base_size, "Fit: Loose (Already at max size)"
|
| 73 |
+
|
| 74 |
+
if preference == "regular":
|
| 75 |
+
if measurements is None:
|
| 76 |
+
return base_size, "Fit: Regular (No measurements provided for adjustment)"
|
| 77 |
+
|
| 78 |
+
# Add 0.5 inches to all measurements
|
| 79 |
+
modified_measurements = {k: v + 0.5 for k, v in measurements.items()}
|
| 80 |
+
new_size, _ = evaluate_size(modified_measurements)
|
| 81 |
+
|
| 82 |
+
if new_size == base_size:
|
| 83 |
+
return new_size, "Fit: Regular (Adjustment didn't change size)"
|
| 84 |
+
else:
|
| 85 |
+
return new_size, f"Fit: Regular (Adjustment pushed size to {new_size})"
|
| 86 |
+
|
| 87 |
+
return base_size, "Unknown preference"
|