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