Spaces:
Sleeping
Sleeping
| """ | |
| EcoVision β Smart Sustainability Analyzer | |
| Gradio 6. Background thread trains model at startup. | |
| All click handlers wrapped in try/except. | |
| """ | |
| import os, io, tempfile, warnings, threading | |
| from datetime import datetime | |
| import numpy as np | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| import gradio as gr | |
| warnings.filterwarnings("ignore") | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| from eco_score import calculate_eco_score, get_score_description | |
| from ml_model import predict_impact, train_model | |
| from llm_analysis import generate_eco_explanation, compare_products_llm | |
| from vision_model import classify_image | |
| # ββ Load CSV (instant) ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DF = pd.read_csv(os.path.join(BASE_DIR, "dataset", "products.csv")) | |
| ALL_PRODUCTS = sorted(DF["product_name"].tolist()) | |
| ALL_CATS = sorted(DF["category"].unique().tolist()) | |
| PACKAGING_OPTS = [ | |
| "Plastic Bottle","Glass Bottle","Cardboard Box","Compostable", | |
| "Recycled Plastic","Single-use Plastic","Paper Wrapper","Other", | |
| ] | |
| # ββ Train model in background thread so UI is never blocked βββββββββββββββββββ | |
| _model_ready = threading.Event() | |
| _model_error = None | |
| def _train_bg(): | |
| global _model_error | |
| try: | |
| model_path = os.path.join(BASE_DIR, "models", "eco_classifier.pkl") | |
| os.makedirs(os.path.join(BASE_DIR, "models"), exist_ok=True) | |
| if not os.path.exists(model_path): | |
| train_model() | |
| _model_ready.set() | |
| print("β Model ready") | |
| except Exception as e: | |
| _model_error = str(e) | |
| _model_ready.set() # unblock anyway so clicks don't hang forever | |
| print(f"β Model training failed: {e}") | |
| threading.Thread(target=_train_bg, daemon=True).start() | |
| def wait_for_model(): | |
| """Wait up to 120s for model, return error string or None.""" | |
| _model_ready.wait(timeout=120) | |
| return _model_error | |
| # ββ Charts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _bar_color(v): | |
| if v > 0.5: return "#C62828" | |
| if v > 0.25: return "#E65100" | |
| return "#2E7D32" | |
| BASE_LAYOUT = dict( | |
| paper_bgcolor="#FFFFFF", | |
| plot_bgcolor="#F4FBF4", | |
| font=dict(color="#1A1A1A", family="sans-serif"), | |
| margin=dict(t=50, b=30, l=55, r=20), | |
| ) | |
| def gauge_chart(score, grade): | |
| gc = {"A":"#2E7D32","B":"#43A047","C":"#E65100", | |
| "D":"#C62828","F":"#6A0000"}.get(grade,"#2E7D32") | |
| fig = go.Figure(go.Indicator( | |
| mode="gauge+number", value=score, | |
| title=dict(text=f"Eco Score β’ Grade {grade}", | |
| font=dict(size=14, color="#1B5E20")), | |
| number=dict(font=dict(size=46, color=gc), suffix=" / 10"), | |
| gauge=dict( | |
| axis=dict(range=[1,10], tickfont=dict(color="#607D63")), | |
| bar=dict(color=gc, thickness=0.28), | |
| bgcolor="#F4FBF4", borderwidth=1, bordercolor="#C8E6C9", | |
| steps=[dict(range=[1,3], color="#FFEBEE"), | |
| dict(range=[3,5], color="#FFF3E0"), | |
| dict(range=[5,7], color="#F1F8E9"), | |
| dict(range=[7,10],color="#E8F5E9")], | |
| threshold=dict(line=dict(color=gc,width=4),thickness=0.8,value=score), | |
| ), | |
| )) | |
| fig.update_layout(**BASE_LAYOUT, height=260) | |
| return fig | |
| def metric_bar(label, value, icon): | |
| color = _bar_color(value) | |
| fig = go.Figure(go.Bar( | |
| x=[label], y=[value], marker_color=[color], | |
| text=[f"{value:.0%}"], textposition="outside", | |
| textfont=dict(color="#1A1A1A", size=14), width=[0.4], | |
| )) | |
| fig.update_layout( | |
| **BASE_LAYOUT, | |
| title=dict(text=f"{icon} {label}", font=dict(color="#1B5E20",size=12)), | |
| yaxis=dict(range=[0,1.3], gridcolor="#D4E8D4", tickformat=".0%", | |
| tickfont=dict(color="#607D63")), | |
| xaxis=dict(showgrid=False), | |
| height=220, | |
| ) | |
| return fig | |
| def prob_bar(probs): | |
| fig = go.Figure(go.Bar( | |
| x=list(probs.keys()), y=list(probs.values()), | |
| marker_color=["#2E7D32","#43A047","#66BB6A"][:len(probs)], | |
| text=[f"{v:.0%}" for v in probs.values()], | |
| textposition="outside", textfont=dict(color="#1A1A1A",size=12), | |
| )) | |
| fig.update_layout( | |
| **BASE_LAYOUT, | |
| title=dict(text="ML Confidence", font=dict(color="#1B5E20",size=12)), | |
| yaxis=dict(range=[0,1.3], gridcolor="#D4E8D4", tickformat=".0%"), | |
| xaxis=dict(showgrid=False), | |
| height=220, | |
| ) | |
| return fig | |
| def comparison_bar(n1, n2, eco1, eco2): | |
| labels = ["Deforestation","Pollution","Biodiversity"] | |
| v1 = [eco1["metrics"]["deforestation_risk"], | |
| eco1["metrics"]["pollution_level"], | |
| eco1["metrics"]["biodiversity_impact"]] | |
| v2 = [eco2["metrics"]["deforestation_risk"], | |
| eco2["metrics"]["pollution_level"], | |
| eco2["metrics"]["biodiversity_impact"]] | |
| fig = go.Figure(data=[ | |
| go.Bar(name=n1[:18], x=labels, y=v1, | |
| text=[f"{v:.0%}" for v in v1], textposition="outside", | |
| marker_color="#2E7D32"), | |
| go.Bar(name=n2[:18], x=labels, y=v2, | |
| text=[f"{v:.0%}" for v in v2], textposition="outside", | |
| marker_color="#C62828"), | |
| ]) | |
| fig.update_layout( | |
| **BASE_LAYOUT, barmode="group", height=280, | |
| title=dict(text="Side-by-Side Metrics",font=dict(color="#1B5E20",size=13)), | |
| yaxis=dict(range=[0,1.4], gridcolor="#D4E8D4", tickformat=".0%"), | |
| xaxis=dict(showgrid=False), | |
| legend=dict(bgcolor="#F4FBF4", bordercolor="#C8E6C9", borderwidth=1), | |
| margin=dict(t=55,b=35,l=55,r=20), | |
| ) | |
| return fig | |
| # ββ HTML helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def err_html(msg): | |
| return (f"<div style='background:#FFEBEE;border:1px solid #EF9A9A;" | |
| f"border-radius:10px;padding:14px;font-family:sans-serif;" | |
| f"color:#C62828;font-size:14px;'>β {msg}</div>") | |
| def ok_html(msg): | |
| return (f"<div style='background:#E8F5E9;border:1px solid #A5D6A7;" | |
| f"border-radius:10px;padding:12px;font-family:sans-serif;" | |
| f"color:#2E7D32;font-size:14px;'>β {msg}</div>") | |
| def score_card(name, eco, ml): | |
| gc = {"A":"#2E7D32","B":"#43A047","C":"#E65100", | |
| "D":"#C62828","F":"#6A0000"}.get(eco["grade"],"#2E7D32") | |
| impact = ml["predicted_impact"] | |
| ibg = {"Low Impact":"#E8F5E9","Medium Impact":"#FFF3E0", | |
| "High Impact":"#FFEBEE"}.get(impact,"#F5F5F5") | |
| ifg = {"Low Impact":"#2E7D32","Medium Impact":"#E65100", | |
| "High Impact":"#C62828"}.get(impact,"#333") | |
| badges = [] | |
| if eco["metrics"]["deforestation_risk"] < 0.2: badges.append("π² Forest Protector") | |
| if eco["metrics"]["pollution_level"] < 0.3: badges.append("π§ Clean Waters") | |
| if eco["metrics"]["biodiversity_impact"] < 0.2: badges.append("π¦ Biodiversity Guard") | |
| if eco["eco_score"] >= 7: badges.append("β Eco Star") | |
| if eco["eco_score"] >= 9: badges.append("π Green Leader") | |
| badge_html = "".join( | |
| f"<span style='display:inline-block;margin:3px;padding:4px 12px;background:#E8F5E9;" | |
| f"color:#2E7D32;border:1px solid #A5D6A7;border-radius:20px;font-size:12px;'>{b}</span>" | |
| for b in badges | |
| ) or "<span style='color:#607D63;font-size:13px;'>No badges yet</span>" | |
| def pill(lbl, val): | |
| bg = "#FFEBEE" if val>0.5 else "#FFF8E1" if val>0.3 else "#E8F5E9" | |
| fg = "#C62828" if val>0.5 else "#E65100" if val>0.3 else "#2E7D32" | |
| return (f"<div style='flex:1;min-width:80px;background:{bg};border-radius:10px;" | |
| f"padding:10px;text-align:center;'>" | |
| f"<div style='font-size:20px;font-weight:700;color:{fg};'>{val:.0%}</div>" | |
| f"<div style='font-size:10px;color:#607D63;font-weight:600;" | |
| f"text-transform:uppercase;letter-spacing:.5px;margin-top:2px;'>{lbl}</div></div>") | |
| return f""" | |
| <div style='background:#fff;border:1px solid #C8E6C9;border-radius:14px; | |
| padding:20px;font-family:sans-serif;margin-top:12px;'> | |
| <h2 style='margin:0 0 10px;color:#1B5E20;font-size:18px;'>{name}</h2> | |
| <div style='display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px;'> | |
| <span style='background:{ibg};color:{ifg};border:2px solid {ifg}; | |
| border-radius:20px;padding:4px 14px;font-size:13px;font-weight:700;'> | |
| {impact}</span> | |
| <span style='color:#607D63;font-size:13px;'> | |
| ML confidence: <strong style='color:#1A1A1A;'>{ml["confidence"]:.0%}</strong></span> | |
| </div> | |
| <div style='display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:16px;'> | |
| <div style='text-align:center;'> | |
| <div style='font-size:60px;font-weight:800;color:{gc};line-height:1;'>{eco["eco_score"]}</div> | |
| <div style='color:#607D63;font-size:11px;'>out of 10</div> | |
| </div> | |
| <div style='border-left:3px solid #C8E6C9;padding-left:14px;flex:1;min-width:150px;'> | |
| <div style='font-size:26px;font-weight:800;color:{gc};'>Grade {eco["grade"]}</div> | |
| <div style='font-size:14px;color:#3E4E3F;'>{eco["grade_label"]}</div> | |
| <div style='font-size:13px;color:#607D63;margin-top:4px;line-height:1.5;'> | |
| {get_score_description(eco["eco_score"])}</div> | |
| </div> | |
| </div> | |
| <div style='display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px;'> | |
| {pill("Deforestation", eco["metrics"]["deforestation_risk"])} | |
| {pill("Pollution", eco["metrics"]["pollution_level"])} | |
| {pill("Biodiversity", eco["metrics"]["biodiversity_impact"])} | |
| </div> | |
| <div style='border-top:1px solid #E8F5E9;padding-top:12px;'> | |
| <div style='color:#607D63;font-size:11px;font-weight:700;text-transform:uppercase; | |
| letter-spacing:1px;margin-bottom:8px;'>Badges</div> | |
| {badge_html} | |
| </div> | |
| </div>""" | |
| def alts_card(category, current=""): | |
| rows = DF[(DF["category"]==category) & | |
| (DF["eco_label"]=="Low Impact") & | |
| (DF["product_name"]!=current)].head(3) | |
| if rows.empty: | |
| return "" | |
| items = "" | |
| for _, r in rows.iterrows(): | |
| ae = calculate_eco_score(r["deforestation_risk"], | |
| r["pollution_level"], r["biodiversity_impact"]) | |
| gc = {"A":"#2E7D32","B":"#43A047","C":"#E65100", | |
| "D":"#C62828"}.get(ae["grade"],"#2E7D32") | |
| items += ( | |
| f"<div style='background:#F1F8F1;border-left:4px solid #2E7D32;" | |
| f"border-radius:0 10px 10px 0;padding:12px 16px;margin:6px 0;" | |
| f"display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;'>" | |
| f"<div><div style='font-weight:700;color:#1B5E20;font-size:14px;'>{r['product_name']}</div>" | |
| f"<div style='color:#607D63;font-size:12px;'>π¦ {r['packaging_type']}</div></div>" | |
| f"<div style='font-size:20px;font-weight:800;color:{gc};'>{ae['eco_score']}" | |
| f"<span style='font-size:12px;color:#607D63;'>/10</span></div></div>" | |
| ) | |
| return (f"<div style='font-family:sans-serif;margin-top:16px;'>" | |
| f"<h3 style='color:#1B5E20;font-size:15px;margin:0 0 8px;'>" | |
| f"π± Eco-Friendly Alternatives</h3>{items}</div>") | |
| def llm_card(text, model_name=""): | |
| out = "" | |
| for line in text.split("\n"): | |
| s = line.strip() | |
| if not s: continue | |
| if s.startswith("## "): | |
| out += (f"<h3 style='color:#1B5E20;font-size:15px;font-weight:700;" | |
| f"margin:14px 0 5px;border-bottom:2px solid #C8E6C9;" | |
| f"padding-bottom:3px;'>{s[3:]}</h3>") | |
| elif len(s)>2 and s[0].isdigit() and s[1]==".": | |
| c = s[3:].replace("**","<strong>",1).replace("**","</strong>",1) | |
| out += (f"<div style='background:#F1F8F1;border-left:4px solid #43A047;" | |
| f"border-radius:0 8px 8px 0;padding:10px 14px;margin:5px 0;" | |
| f"color:#1A1A1A;font-size:13px;'>#{s[0]} {c}</div>") | |
| elif s.startswith("- "): | |
| out += f"<li style='color:#1A1A1A;font-size:13px;margin:3px 0;'>{s[2:]}</li>" | |
| else: | |
| s2 = s.replace("**","<strong>",1).replace("**","</strong>",1) | |
| out += (f"<p style='color:#1A1A1A;font-size:13px;line-height:1.7;" | |
| f"margin:5px 0;'>{s2}</p>") | |
| badge = (f"<span style='background:#F1F8F1;border:1px solid #C8E6C9;border-radius:20px;" | |
| f"padding:2px 10px;font-size:11px;color:#607D63;'>{model_name}</span>" | |
| ) if model_name else "" | |
| return (f"<div style='background:#fff;border:1px solid #C8E6C9;border-radius:14px;" | |
| f"padding:20px;font-family:sans-serif;margin-top:16px;'>" | |
| f"<h3 style='color:#1B5E20;font-size:16px;margin:0 0 12px;'>" | |
| f"π§ AI Environmental Analysis {badge}</h3><div>{out}</div></div>") | |
| def compare_card(p, eco, ml, winner): | |
| gc = {"A":"#2E7D32","B":"#43A047","C":"#E65100", | |
| "D":"#C62828","F":"#6A0000"}.get(eco["grade"],"#2E7D32") | |
| border = "#2E7D32" if winner else "#E0E0E0" | |
| pills = "".join( | |
| f"<div style='background:#F5F5F5;border-radius:8px;padding:7px;text-align:center;'>" | |
| f"<div style='font-size:13px;font-weight:700;color:{_bar_color(v)};'>{v:.0%}</div>" | |
| f"<div style='font-size:10px;color:#607D63;'>{l}</div></div>" | |
| for l,v in [("π² Deforest.",eco["metrics"]["deforestation_risk"]), | |
| ("π¨ Pollution",eco["metrics"]["pollution_level"]), | |
| ("π¦ Biodiv.",eco["metrics"]["biodiversity_impact"])] | |
| ) | |
| return (f"<div style='background:#fff;border:2px solid {border};border-radius:14px;" | |
| f"padding:16px;font-family:sans-serif;'>" | |
| f"<div style='font-size:15px;font-weight:700;color:#1B5E20;margin-bottom:4px;'>" | |
| f"{'π ' if winner else ''}{p['product_name']}</div>" | |
| f"<div style='color:#607D63;font-size:12px;margin-bottom:12px;'>" | |
| f"{p['category']} Β· {p['packaging_type']}</div>" | |
| f"<div style='display:flex;gap:12px;margin-bottom:12px;'>" | |
| f"<div style='text-align:center;'>" | |
| f"<div style='font-size:48px;font-weight:800;color:{gc};line-height:1;'>" | |
| f"{eco['eco_score']}</div>" | |
| f"<div style='color:#607D63;font-size:10px;'>SCORE</div></div>" | |
| f"<div style='text-align:center;'>" | |
| f"<div style='font-size:40px;font-weight:800;color:{gc};line-height:1;'>" | |
| f"{eco['grade']}</div>" | |
| f"<div style='color:#607D63;font-size:10px;'>GRADE</div></div></div>" | |
| f"<div style='display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;" | |
| f"margin-bottom:10px;'>{pills}</div>" | |
| f"<div style='font-size:12px;color:#3E4E3F;background:#F9FBF9;" | |
| f"border-radius:8px;padding:8px 12px;'>" | |
| f"ML: <strong>{ml['predicted_impact']}</strong> Β· " | |
| f"Conf: <strong>{ml['confidence']:.0%}</strong></div></div>") | |
| # ββ PDF βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save_pdf(name, category, eco, ml, explanation, packaging=""): | |
| try: | |
| from reportlab.lib.pagesizes import letter | |
| from reportlab.lib.styles import ParagraphStyle | |
| from reportlab.lib.colors import HexColor | |
| from reportlab.platypus import (SimpleDocTemplate, Paragraph, | |
| Spacer, Table, TableStyle) | |
| from reportlab.lib.units import inch | |
| buf = io.BytesIO() | |
| doc = SimpleDocTemplate(buf, pagesize=letter, topMargin=inch, | |
| bottomMargin=inch, leftMargin=inch, rightMargin=inch) | |
| TT = ParagraphStyle("TT",fontSize=20,fontName="Helvetica-Bold", | |
| textColor=HexColor("#1B5E20"),spaceAfter=6) | |
| HH = ParagraphStyle("HH",fontSize=13,fontName="Helvetica-Bold", | |
| textColor=HexColor("#2E7D32"),spaceAfter=4,spaceBefore=8) | |
| BB = ParagraphStyle("BB",fontSize=10,fontName="Helvetica", | |
| textColor=HexColor("#1A1A1A"),spaceAfter=3,leading=14) | |
| content = [ | |
| Paragraph("EcoVision Sustainability Report", TT), | |
| Paragraph(f"Generated: {datetime.now().strftime('%B %d, %Y %H:%M')}", BB), | |
| Spacer(1,0.15*inch), Paragraph("Product Overview", HH), | |
| ] | |
| rows = [["Product",name],["Category",category], | |
| ["Eco Score",f"{eco['eco_score']}/10 Grade {eco['grade']}"], | |
| ["Impact",ml.get("predicted_impact","N/A")],["Packaging",packaging]] | |
| tbl = Table(rows, colWidths=[1.8*inch, 4.7*inch]) | |
| tbl.setStyle(TableStyle([ | |
| ("BACKGROUND",(0,0),(0,-1),HexColor("#E8F5E9")), | |
| ("TEXTCOLOR",(0,0),(0,-1),HexColor("#1B5E20")), | |
| ("TEXTCOLOR",(1,0),(1,-1),HexColor("#1A1A1A")), | |
| ("FONTNAME",(0,0),(-1,-1),"Helvetica"), | |
| ("FONTNAME",(0,0),(0,-1),"Helvetica-Bold"), | |
| ("FONTSIZE",(0,0),(-1,-1),10), | |
| ("GRID",(0,0),(-1,-1),0.5,HexColor("#C8E6C9")), | |
| ("ROWBACKGROUNDS",(0,0),(-1,-1),[HexColor("#FFFFFF"),HexColor("#F7F9F4")]), | |
| ("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5), | |
| ])) | |
| content += [tbl, Spacer(1,0.12*inch), Paragraph("Analysis", HH)] | |
| for line in explanation.replace("**","").replace("## ","").split("\n")[:30]: | |
| if line.strip(): | |
| content.append(Paragraph(line.strip(), BB)) | |
| doc.build(content) | |
| data = buf.getvalue() | |
| except Exception: | |
| data = (f"EcoVision Report\nProduct: {name}\n" | |
| f"Eco Score: {eco['eco_score']}/10\nGrade: {eco['grade']}\n" | |
| f"Impact: {ml.get('predicted_impact')}\n\n{explanation}").encode("utf-8") | |
| ext = "pdf" if data[:4]==b"%PDF" else "txt" | |
| path = os.path.join(tempfile.gettempdir(), | |
| f"eco_{name[:20].replace(' ','_')}.{ext}") | |
| with open(path,"wb") as f: f.write(data) | |
| return path | |
| # ββ Core pipeline βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_eco(name, category, deforest, pollution, biodiversity, | |
| packaging, ingredients, groq_key): | |
| if groq_key: | |
| os.environ["GROQ_API_KEY"] = groq_key.strip() | |
| eco = calculate_eco_score(deforest, pollution, biodiversity) | |
| ml = predict_impact(category, deforest, pollution, biodiversity) | |
| llm = generate_eco_explanation( | |
| name, category, deforest, pollution, biodiversity, | |
| eco["eco_score"], ml["predicted_impact"], packaging, ingredients, | |
| ) | |
| return eco, ml, llm | |
| EMPTY9 = ("", None, None, None, None, None, "", "", None) | |
| # ββ Tab handlers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def search_fn(query, groq_key): | |
| try: | |
| model_err = wait_for_model() | |
| if model_err: | |
| return (err_html(f"Model training failed: {model_err}"),) + EMPTY9[1:] | |
| if not query.strip(): | |
| return (err_html("Please enter a product name."),) + EMPTY9[1:] | |
| q = query.lower() | |
| m = DF[DF["product_name"].str.lower().str.contains(q, na=False)] | |
| if m.empty: | |
| m = DF[DF["category"].str.lower().str.contains(q, na=False)] | |
| if m.empty: | |
| return (err_html(f"'{query}' not found. Try: Patagonia Β· Lays Β· " | |
| f"Head & Shoulders Β· Shein Β· Ethique"),) + EMPTY9[1:] | |
| row = m.iloc[0] | |
| eco, ml, llm = run_eco( | |
| row["product_name"], row["category"], | |
| float(row["deforestation_risk"]), float(row["pollution_level"]), | |
| float(row["biodiversity_impact"]), row["packaging_type"], | |
| row["ingredients"], groq_key, | |
| ) | |
| pdf = save_pdf(row["product_name"], row["category"], eco, ml, | |
| llm["explanation"], row["packaging_type"]) | |
| return ( | |
| score_card(row["product_name"], eco, ml), | |
| gauge_chart(eco["eco_score"], eco["grade"]), | |
| metric_bar("Deforestation Risk", eco["metrics"]["deforestation_risk"], "π²"), | |
| metric_bar("Pollution Level", eco["metrics"]["pollution_level"], "π¨"), | |
| metric_bar("Biodiversity Impact",eco["metrics"]["biodiversity_impact"],"π¦"), | |
| prob_bar(ml["all_probabilities"]), | |
| llm_card(llm["explanation"], llm.get("model","")), | |
| alts_card(row["category"], row["product_name"]), | |
| pdf, | |
| ) | |
| except Exception as e: | |
| return (err_html(f"Error: {str(e)}"),) + EMPTY9[1:] | |
| def manual_fn(name, category, packaging, ingredients, | |
| deforest, pollution, biodiversity, groq_key): | |
| try: | |
| model_err = wait_for_model() | |
| if model_err: | |
| return (err_html(f"Model training failed: {model_err}"),) + EMPTY9[1:] | |
| if not name.strip(): | |
| return (err_html("Please enter a product name."),) + EMPTY9[1:] | |
| eco, ml, llm = run_eco(name, category, deforest, pollution, biodiversity, | |
| packaging, ingredients, groq_key) | |
| pdf = save_pdf(name, category, eco, ml, llm["explanation"], packaging) | |
| return ( | |
| score_card(name, eco, ml), | |
| gauge_chart(eco["eco_score"], eco["grade"]), | |
| metric_bar("Deforestation Risk", eco["metrics"]["deforestation_risk"], "π²"), | |
| metric_bar("Pollution Level", eco["metrics"]["pollution_level"], "π¨"), | |
| metric_bar("Biodiversity Impact",eco["metrics"]["biodiversity_impact"],"π¦"), | |
| prob_bar(ml["all_probabilities"]), | |
| llm_card(llm["explanation"], llm.get("model","")), | |
| alts_card(category, ""), | |
| pdf, | |
| ) | |
| except Exception as e: | |
| return (err_html(f"Error: {str(e)}"),) + EMPTY9[1:] | |
| EMPTY_IMG = ("", None, "", None, None, None, None, "", None) | |
| def image_fn(image_path, groq_key): | |
| try: | |
| model_err = wait_for_model() | |
| if model_err: | |
| return (err_html(f"Model training failed: {model_err}"),) + EMPTY_IMG[1:] | |
| if not image_path: | |
| return EMPTY_IMG | |
| vision = classify_image(image_path) | |
| detected = vision["predicted_category"] | |
| cat_rows = DF[DF["category"]==detected] | |
| if cat_rows.empty: | |
| return EMPTY_IMG | |
| avg = cat_rows[["deforestation_risk","pollution_level","biodiversity_impact"]].mean() | |
| rep = cat_rows.iloc[0] | |
| eco, ml, llm = run_eco( | |
| f"Detected: {detected}", detected, | |
| float(avg["deforestation_risk"]), float(avg["pollution_level"]), | |
| float(avg["biodiversity_impact"]), rep["packaging_type"], "", groq_key, | |
| ) | |
| scores = vision["all_scores"] | |
| fig_v = go.Figure(go.Bar( | |
| x=list(scores.keys()), y=list(scores.values()), | |
| marker_color=["#2E7D32" if k==detected else "#A5D6A7" for k in scores], | |
| text=[f"{v:.0%}" for v in scores.values()], | |
| textposition="outside", textfont=dict(color="#1A1A1A", size=12), | |
| )) | |
| fig_v.update_layout( | |
| **BASE_LAYOUT, height=220, | |
| title=dict(text=f"Vision β '{detected}' detected", | |
| font=dict(color="#1B5E20",size=12)), | |
| yaxis=dict(range=[0,1.3], gridcolor="#D4E8D4", tickformat=".0%"), | |
| xaxis=dict(showgrid=False), | |
| ) | |
| vhtml = (f"<div style='background:#fff;border:1px solid #C8E6C9;" | |
| f"border-radius:12px;padding:16px;font-family:sans-serif;'>" | |
| f"<div style='color:#607D63;font-size:11px;font-weight:700;" | |
| f"text-transform:uppercase;letter-spacing:1px;'>Detected</div>" | |
| f"<div style='font-size:26px;font-weight:800;color:#1B5E20;'>" | |
| f"π· {detected}</div>" | |
| f"<div style='color:#607D63;font-size:13px;margin-top:4px;'>" | |
| f"Confidence: <strong style='color:#2E7D32;'>" | |
| f"{vision['confidence']:.0%}</strong> Β· {vision['method']}</div></div>") | |
| pdf = save_pdf(f"Detected {detected}", detected, eco, ml, | |
| llm["explanation"], rep["packaging_type"]) | |
| return (vhtml, fig_v, score_card(f"Detected {detected}", eco, ml), | |
| gauge_chart(eco["eco_score"], eco["grade"]), | |
| metric_bar("Deforestation Risk",eco["metrics"]["deforestation_risk"],"π²"), | |
| metric_bar("Pollution Level", eco["metrics"]["pollution_level"], "π¨"), | |
| metric_bar("Biodiversity Impact",eco["metrics"]["biodiversity_impact"],"π¦"), | |
| llm_card(llm["explanation"], llm.get("model","")), pdf) | |
| except Exception as e: | |
| return (err_html(f"Error: {str(e)}"),) + EMPTY_IMG[1:] | |
| EMPTY_CMP = ("","",None,None,None,"") | |
| def compare_fn(n1, n2, groq_key): | |
| try: | |
| model_err = wait_for_model() | |
| if model_err: | |
| return (err_html(f"Model training failed: {model_err}"),) + EMPTY_CMP[1:] | |
| if groq_key: | |
| os.environ["GROQ_API_KEY"] = groq_key.strip() | |
| p1 = DF[DF["product_name"]==n1].iloc[0] | |
| p2 = DF[DF["product_name"]==n2].iloc[0] | |
| eco1 = calculate_eco_score(float(p1["deforestation_risk"]), | |
| float(p1["pollution_level"]), | |
| float(p1["biodiversity_impact"])) | |
| eco2 = calculate_eco_score(float(p2["deforestation_risk"]), | |
| float(p2["pollution_level"]), | |
| float(p2["biodiversity_impact"])) | |
| ml1 = predict_impact(p1["category"],float(p1["deforestation_risk"]), | |
| float(p1["pollution_level"]),float(p1["biodiversity_impact"])) | |
| ml2 = predict_impact(p2["category"],float(p2["deforestation_risk"]), | |
| float(p2["pollution_level"]),float(p2["biodiversity_impact"])) | |
| d1 = dict(name=n1, category=p1["category"], | |
| eco_score=eco1["eco_score"], **eco1["metrics"]) | |
| d2 = dict(name=n2, category=p2["category"], | |
| eco_score=eco2["eco_score"], **eco2["metrics"]) | |
| comp = compare_products_llm(d1, d2) | |
| w1 = eco1["eco_score"] >= eco2["eco_score"] | |
| return (compare_card(p1,eco1,ml1,w1), compare_card(p2,eco2,ml2,not w1), | |
| gauge_chart(eco1["eco_score"],eco1["grade"]), | |
| gauge_chart(eco2["eco_score"],eco2["grade"]), | |
| comparison_bar(n1,n2,eco1,eco2), | |
| llm_card(comp["comparison"],comp.get("model",""))) | |
| except Exception as e: | |
| return (err_html(f"Error: {str(e)}"), "",None,None,None,"") | |
| # ββ Build UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(title="EcoVision β Smart Sustainability Analyzer") as demo: | |
| gr.HTML(""" | |
| <div style='text-align:center;padding:28px 16px 18px; | |
| background:linear-gradient(135deg,#E8F5E9,#F1F8E9); | |
| border-bottom:2px solid #C8E6C9;'> | |
| <div style='font-size:44px;font-weight:800;color:#1B5E20;'>πΏ EcoVision</div> | |
| <p style='color:#3E4E3F;font-size:14px;margin:8px 0 12px;'> | |
| Smart Sustainability Analyzer Β· ML Β· Computer Vision Β· LLM | |
| </p> | |
| <div style='display:flex;justify-content:center;gap:8px;flex-wrap:wrap;'> | |
| <span style='background:#fff;border:1px solid #C8E6C9;border-radius:20px; | |
| padding:3px 12px;font-size:12px;font-weight:600;color:#2E7D32;'> | |
| β Rule-based Scoring</span> | |
| <span style='background:#fff;border:1px solid #C8E6C9;border-radius:20px; | |
| padding:3px 12px;font-size:12px;font-weight:600;color:#2E7D32;'> | |
| π€ RandomForest ML</span> | |
| <span style='background:#fff;border:1px solid #C8E6C9;border-radius:20px; | |
| padding:3px 12px;font-size:12px;font-weight:600;color:#2E7D32;'> | |
| π· MobileNetV2 CV</span> | |
| <span style='background:#fff;border:1px solid #C8E6C9;border-radius:20px; | |
| padding:3px 12px;font-size:12px;font-weight:600;color:#2E7D32;'> | |
| π§ Groq LLM</span> | |
| </div> | |
| </div>""") | |
| with gr.Accordion("π Groq API Key (optional)", open=False): | |
| gr.HTML("<p style='color:#3E4E3F;font-size:13px;margin:4px 0 8px;'>" | |
| "Free at <a href='https://console.groq.com' target='_blank' " | |
| "style='color:#2E7D32;font-weight:600;'>console.groq.com</a> β " | |
| "works without it using template explanations.</p>") | |
| groq_key = gr.Textbox(label="Groq API Key", type="password", placeholder="gsk_β¦") | |
| with gr.Tabs(): | |
| # ββ Tab 1: Search βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Product Search"): | |
| gr.HTML("<p style='color:#3E4E3F;font-size:14px;margin:10px 0;'>" | |
| "Search any of our <strong style='color:#1B5E20;'>53+ products</strong>" | |
| " β shampoos, snacks, clothing, cosmetics, plastics.</p>") | |
| with gr.Row(): | |
| s_in = gr.Textbox( | |
| label="Product name or category", | |
| placeholder="e.g. Head & Shoulders | Patagonia | Lays | Shein", | |
| scale=4, | |
| ) | |
| s_btn = gr.Button("π¬ Analyze", variant="primary", scale=1) | |
| with gr.Accordion("π Browse all products", open=False): | |
| gr.Dataframe( | |
| value=DF[["product_name","category","eco_label","packaging_type"]].rename( | |
| columns={"product_name":"Product","category":"Category", | |
| "eco_label":"Impact","packaging_type":"Packaging"}), | |
| interactive=False, | |
| ) | |
| s_card = gr.HTML() | |
| with gr.Row(): | |
| s_gauge = gr.Plot() | |
| s_prob = gr.Plot() | |
| with gr.Row(): | |
| s_b1 = gr.Plot() | |
| s_b2 = gr.Plot() | |
| s_b3 = gr.Plot() | |
| s_llm = gr.HTML() | |
| s_alts = gr.HTML() | |
| s_pdf = gr.File(label="π Download Report") | |
| s_btn.click( | |
| fn=search_fn, | |
| inputs=[s_in, groq_key], | |
| outputs=[s_card, s_gauge, s_b1, s_b2, s_b3, s_prob, | |
| s_llm, s_alts, s_pdf], | |
| ) | |
| # ββ Tab 2: Manual βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("βοΈ Manual Input"): | |
| gr.HTML("<p style='color:#3E4E3F;font-size:14px;margin:10px 0;'>" | |
| "Analyse any custom product by entering details manually.</p>") | |
| with gr.Row(): | |
| with gr.Column(): | |
| m_name = gr.Textbox(label="Product Name", | |
| placeholder="e.g. My Eco Shampoo") | |
| m_cat = gr.Dropdown(ALL_CATS, label="Category", value="Shampoo") | |
| m_pkg = gr.Dropdown(PACKAGING_OPTS, label="Packaging", | |
| value="Plastic Bottle") | |
| m_ingr = gr.Textbox(label="Ingredients / Materials", | |
| placeholder="e.g. Organic Cotton, Natural Dyes", | |
| lines=3) | |
| with gr.Column(): | |
| m_defo = gr.Slider(0.0,1.0,value=0.30,step=0.05,label="π² Deforestation Risk") | |
| m_poll = gr.Slider(0.0,1.0,value=0.50,step=0.05,label="π¨ Pollution Level") | |
| m_bio = gr.Slider(0.0,1.0,value=0.40,step=0.05,label="π¦ Biodiversity Impact") | |
| m_btn = gr.Button("π¬ Analyse Product", variant="primary") | |
| m_card = gr.HTML() | |
| with gr.Row(): | |
| m_gauge = gr.Plot() | |
| m_prob = gr.Plot() | |
| with gr.Row(): | |
| m_b1 = gr.Plot() | |
| m_b2 = gr.Plot() | |
| m_b3 = gr.Plot() | |
| m_llm = gr.HTML() | |
| m_alts = gr.HTML() | |
| m_pdf = gr.File(label="π Download Report") | |
| m_btn.click( | |
| fn=manual_fn, | |
| inputs=[m_name, m_cat, m_pkg, m_ingr, | |
| m_defo, m_poll, m_bio, groq_key], | |
| outputs=[m_card, m_gauge, m_b1, m_b2, m_b3, m_prob, | |
| m_llm, m_alts, m_pdf], | |
| ) | |
| # ββ Tab 3: Image ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π· Image Scan"): | |
| gr.HTML("<p style='color:#3E4E3F;font-size:14px;margin:10px 0;'>" | |
| "Upload a product photo β MobileNetV2 detects the category.</p>") | |
| with gr.Row(): | |
| with gr.Column(): | |
| i_img = gr.Image(type="filepath", label="Upload Product Image", | |
| height=250) | |
| i_btn = gr.Button("π Scan & Analyse", variant="primary") | |
| with gr.Column(): | |
| i_detect = gr.HTML() | |
| i_vchart = gr.Plot() | |
| i_card = gr.HTML() | |
| with gr.Row(): | |
| i_gauge = gr.Plot() | |
| with gr.Row(): | |
| i_b1 = gr.Plot() | |
| i_b2 = gr.Plot() | |
| i_b3 = gr.Plot() | |
| i_llm = gr.HTML() | |
| i_pdf = gr.File(label="π Download Report") | |
| i_btn.click( | |
| fn=image_fn, | |
| inputs=[i_img, groq_key], | |
| outputs=[i_detect, i_vchart, i_card, i_gauge, | |
| i_b1, i_b2, i_b3, i_llm, i_pdf], | |
| ) | |
| # ββ Tab 4: Compare ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("βοΈ Compare Products"): | |
| gr.HTML("<p style='color:#3E4E3F;font-size:14px;margin:10px 0;'>" | |
| "Compare any two products side-by-side.</p>") | |
| with gr.Row(): | |
| c_p1 = gr.Dropdown(ALL_PRODUCTS, label="π °οΈ Product A", | |
| value=ALL_PRODUCTS[0], scale=5) | |
| gr.HTML("<div style='padding:22px 6px 0;font-size:18px;" | |
| "font-weight:800;color:#2E7D32;'>VS</div>") | |
| c_p2 = gr.Dropdown(ALL_PRODUCTS, label="π ±οΈ Product B", | |
| value=ALL_PRODUCTS[5], scale=5) | |
| c_btn = gr.Button("βοΈ Compare", variant="primary", scale=2) | |
| with gr.Row(): | |
| c_card1 = gr.HTML() | |
| c_card2 = gr.HTML() | |
| with gr.Row(): | |
| c_g1 = gr.Plot() | |
| c_g2 = gr.Plot() | |
| c_bar = gr.Plot() | |
| c_llm = gr.HTML() | |
| c_btn.click( | |
| fn=compare_fn, | |
| inputs=[c_p1, c_p2, groq_key], | |
| outputs=[c_card1, c_card2, c_g1, c_g2, c_bar, c_llm], | |
| ) | |
| gr.HTML("<div style='text-align:center;color:#607D63;font-size:12px;" | |
| "padding:14px;border-top:1px solid #C8E6C9;margin-top:16px;" | |
| "background:#F1F8F1;'>" | |
| "πΏ <strong>EcoVision</strong> Β· Gradio Β· scikit-learn Β· " | |
| "PyTorch MobileNetV2 Β· Groq LLM Β· Plotly</div>") | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) |