import os import uuid import shutil import tempfile import datetime import pandas as pd import gradio as gr from PIL import Image # ========================= # PDF (reportlab) # ========================= from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak from reportlab.platypus import Image as RLImage from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.pagesizes import A4 from reportlab.lib.units import cm # ========================= # CONFIG # ========================= XLSX_PATH = "Soild Anaylsis EN(1).xlsx" IND_SHEET = "Indicators" SEASON_COLS = { "Fall": "Available in Fall", "Winter": "Available in Winter", "Spring": "Available in Spring", "Summer": "Available in Summer", } SCORE_MAP = {"Low": 1, "Medium": 2, "High": 3} # ========================= # LOAD DATA # ========================= if not os.path.exists(XLSX_PATH): raise FileNotFoundError("Missing Soil Analysis Excel file") ind_df = pd.read_excel(XLSX_PATH, sheet_name=IND_SHEET) # ========================= # HELPERS # ========================= def normalize_text(x): if pd.isna(x): return "" return str(x).strip() def is_available(row, season): return normalize_text(row.get(SEASON_COLS[season], "")).lower() == "x" def build_indicators(season): return [r for _, r in ind_df.iterrows() if is_available(r, season)] def requires_self_assessment(data_entry): data_entry = data_entry.lower() return any( k in data_entry for k in ["smell", "odor", "temperature", "moisture", "value", "on site"] ) # ========================= # SAFE IMAGE HANDLING (FIX) # ========================= def save_uploaded_images(uploaded): """ Normalize Gradio uploads into safe local file paths. Accepts PIL, str, tuple, list of them. """ if not uploaded: return [] if not isinstance(uploaded, list): uploaded = [uploaded] saved = [] for item in uploaded: if isinstance(item, tuple): item = item[0] if isinstance(item, str) and os.path.exists(item): dst = f"/tmp/{uuid.uuid4().hex}{os.path.splitext(item)[1]}" shutil.copy(item, dst) saved.append(dst) elif isinstance(item, Image.Image): dst = f"/tmp/{uuid.uuid4().hex}.png" item.save(dst) saved.append(dst) return saved # ========================= # PLACEHOLDER "MODEL" # ========================= def visual_score_placeholder(image_paths, indicator_name): """ Lightweight heuristic placeholder. Replace later with CLIP / ViT safely. """ if not image_paths: return "Medium", "No relevant photo detected; defaulting to Medium." name = indicator_name.lower() if "earthworm" in name or "fauna" in name: return "High", "Visible earthworms indicate active soil biology." if "erosion" in name: return "Low", "Visible rills and sediment indicate erosion." if "structure" in name: return "Medium", "Soil aggregates visible but not clearly crumbly." if "cover" in name: return "High", "Dense vegetation cover visible." return "Medium", "Visual indicators are inconclusive." # ========================= # PDF REPORT # ========================= def soil_quality_label(total): if 15 <= total <= 25: return "Low soil quality" if 26 <= total <= 37: return "Medium soil quality" if 38 <= total <= 48: return "High soil quality" return "Outside expected range" def generate_pdf_report(state): tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") doc = SimpleDocTemplate(tmp.name, pagesize=A4) styles = getSampleStyleSheet() story = [] story.append(Paragraph("Soil Quality Assessment Report", styles["Title"])) story.append(Spacer(1, 12)) story.append(Paragraph(f"Season: {state['season']}", styles["Normal"])) story.append(Paragraph(f"Location: {state['location']}", styles["Normal"])) story.append(Paragraph(f"Date: {datetime.date.today().isoformat()}", styles["Normal"])) story.append(Spacer(1, 20)) total = 0 for r in state["results"]: total += r["score"] story.append(Paragraph( f"{r['id']}. {r['name']}", styles["Heading2"] )) if r.get("image"): story.append(RLImage(r["image"], width=8*cm, height=6*cm)) story.append(Spacer(1, 6)) story.append(Paragraph( f"Assessment: {r['assessment']} ({r['score']} points)", styles["Normal"] )) story.append(Paragraph( f"Explanation:
{r['explanation']}", styles["Normal"] )) story.append(Paragraph( f"Motivation:
{r['motivation']}", styles["Normal"] )) story.append(Spacer(1, 12)) story.append(PageBreak()) story.append(Paragraph("Overall Result", styles["Heading1"])) story.append(Paragraph(f"Total score: {total} / 48", styles["Normal"])) story.append(Paragraph( f"Soil quality classification: {soil_quality_label(total)}", styles["Normal"] )) doc.build(story) tmp.close() return tmp.name # ========================= # CORE LOGIC # ========================= def run_assessment(season, location, uploaded_images, self_scores): if self_scores is None: self_scores = {} image_paths = save_uploaded_images(uploaded_images) indicators = build_indicators(season) results = [] for _, row in enumerate(indicators): ind_id = int(row["Indicator ID"]) name = normalize_text(row["Indicator Name"]) data_entry = normalize_text(row["Data entry"]) motivation = normalize_text(row["Motivation"]) if requires_self_assessment(data_entry): label = self_scores.get(str(ind_id), "Medium") explanation = ( "This indicator cannot be reliably assessed from photos alone. " "The score is based on the user's self-assessment." ) img = None else: label, explanation = visual_score_placeholder(image_paths, name) img = image_paths[0] if image_paths else None results.append({ "id": ind_id, "name": name, "assessment": label, "score": SCORE_MAP[label], "motivation": motivation, "explanation": explanation, "image": img }) state = { "season": season, "location": location, "results": results } pdf = generate_pdf_report(state) return pdf # ========================= # UI # ========================= with gr.Blocks(title="Soil Quality Analyzer") as demo: gr.Markdown("# 🌱 Soil Quality Analyzer (Photo-Based)") season = gr.Dropdown(["Fall", "Winter", "Spring", "Summer"], value="Spring", label="Season") location = gr.Textbox(label="Location") uploads = gr.File( file_types=["image"], file_count="multiple", label="Upload all field photos" ) gr.Markdown("### Self-assessment (only required where photos cannot assess)") self_scores = gr.JSON(label="Self scores (e.g. {'3':'High','6':'Low'})") run_btn = gr.Button("Run assessment") pdf_out = gr.File(label="📄 Download PDF report") run_btn.click( run_assessment, inputs=[season, location, uploads, self_scores], outputs=[pdf_out] ) demo.launch()