Spaces:
Sleeping
Sleeping
| 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("<b>Soil Quality Assessment Report</b>", 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"<b>{r['id']}. {r['name']}</b>", | |
| 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"<b>Assessment:</b> {r['assessment']} ({r['score']} points)", | |
| styles["Normal"] | |
| )) | |
| story.append(Paragraph( | |
| f"<b>Explanation:</b><br/>{r['explanation']}", | |
| styles["Normal"] | |
| )) | |
| story.append(Paragraph( | |
| f"<b>Motivation:</b><br/>{r['motivation']}", | |
| styles["Normal"] | |
| )) | |
| story.append(Spacer(1, 12)) | |
| story.append(PageBreak()) | |
| story.append(Paragraph("<b>Overall Result</b>", styles["Heading1"])) | |
| story.append(Paragraph(f"<b>Total score:</b> {total} / 48", styles["Normal"])) | |
| story.append(Paragraph( | |
| f"<b>Soil quality classification:</b> {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() | |