""" 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"
❌ {msg}
") def ok_html(msg): return (f"
✅ {msg}
") 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"{b}" for b in badges ) or "No badges yet" 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"
" f"
{val:.0%}
" f"
{lbl}
") return f"""

{name}

{impact} ML confidence: {ml["confidence"]:.0%}
{eco["eco_score"]}
out of 10
Grade {eco["grade"]}
{eco["grade_label"]}
{get_score_description(eco["eco_score"])}
{pill("Deforestation", eco["metrics"]["deforestation_risk"])} {pill("Pollution", eco["metrics"]["pollution_level"])} {pill("Biodiversity", eco["metrics"]["biodiversity_impact"])}
Badges
{badge_html}
""" 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"
" f"
{r['product_name']}
" f"
📦 {r['packaging_type']}
" f"
{ae['eco_score']}" f"/10
" ) return (f"
" f"

" f"🌱 Eco-Friendly Alternatives

{items}
") 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"

{s[3:]}

") elif len(s)>2 and s[0].isdigit() and s[1]==".": c = s[3:].replace("**","",1).replace("**","",1) out += (f"
#{s[0]} {c}
") elif s.startswith("- "): out += f"
  • {s[2:]}
  • " else: s2 = s.replace("**","",1).replace("**","",1) out += (f"

    {s2}

    ") badge = (f"{model_name}" ) if model_name else "" return (f"
    " f"

    " f"🧠 AI Environmental Analysis {badge}

    {out}
    ") 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"
    " f"
    {v:.0%}
    " f"
    {l}
    " for l,v in [("🌲 Deforest.",eco["metrics"]["deforestation_risk"]), ("💨 Pollution",eco["metrics"]["pollution_level"]), ("🦋 Biodiv.",eco["metrics"]["biodiversity_impact"])] ) return (f"
    " f"
    " f"{'🏆 ' if winner else ''}{p['product_name']}
    " f"
    " f"{p['category']} · {p['packaging_type']}
    " f"
    " f"
    " f"
    " f"{eco['eco_score']}
    " f"
    SCORE
    " f"
    " f"
    " f"{eco['grade']}
    " f"
    GRADE
    " f"
    {pills}
    " f"
    " f"ML: {ml['predicted_impact']} · " f"Conf: {ml['confidence']:.0%}
    ") # ── 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"
    " f"
    Detected
    " f"
    " f"📷 {detected}
    " f"
    " f"Confidence: " f"{vision['confidence']:.0%} · {vision['method']}
    ") 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("""
    🌿 EcoVision

    Smart Sustainability Analyzer  ·  ML · Computer Vision · LLM

    ✅ Rule-based Scoring 🤖 RandomForest ML 📷 MobileNetV2 CV 🧠 Groq LLM
    """) with gr.Accordion("🔑 Groq API Key (optional)", open=False): gr.HTML("

    " "Free at console.groq.com — " "works without it using template explanations.

    ") 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("

    " "Search any of our 53+ products" " — shampoos, snacks, clothing, cosmetics, plastics.

    ") 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("

    " "Analyse any custom product by entering details manually.

    ") 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("

    " "Upload a product photo — MobileNetV2 detects the category.

    ") 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("

    " "Compare any two products side-by-side.

    ") with gr.Row(): c_p1 = gr.Dropdown(ALL_PRODUCTS, label="🅰️ Product A", value=ALL_PRODUCTS[0], scale=5) gr.HTML("
    VS
    ") 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("
    " "🌿 EcoVision · Gradio · scikit-learn · " "PyTorch MobileNetV2 · Groq LLM · Plotly
    ") if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)