""" Soil Quality Assessment App Methodology based on: Luján Soto, R. (2018). Cuaderno de campo para la evaluación de prácticas regenerativas sobre la calidad del suelo. Universidad de Córdoba – CEBAS-CSIC. Educational inspiration: Ecorestauración – https://ecorestauracion.es/curso/ """ import re import os import datetime import tempfile import pandas as pd import gradio as gr # ========================= # 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 = "Soil Anaylsis.xlsx" IND_SHEET = "Indicators" OVERALL_SHEET = "Overall Score" INDICATOR_IMAGE_EXT = ".png" # 1.png, 2.png, ... 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(f"Missing file: {XLSX_PATH}") ind_df = pd.read_excel(XLSX_PATH, sheet_name=IND_SHEET) # ========================= # HELPERS # ========================= def normalize_text(s): if s is None or (isinstance(s, float) and pd.isna(s)): return "" return str(s).strip() def is_available(row, season): return normalize_text(row.get(SEASON_COLS[season], "")).lower() == "x" def build_season_indicator_list(season): return [r for _, r in ind_df.iterrows() if is_available(r, season)] def get_indicator_image(indicator_id): path = f"{indicator_id}{INDICATOR_IMAGE_EXT}" return path if os.path.exists(path) else None def save_user_image(image): if image is None: return None tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png") image.save(tmp.name) tmp.close() return tmp.name def indicator_prompt(row, idx, total): return f""" ### Indicator {idx + 1} / {total} **{int(row['Indicator ID'])}. {row['Indicator Name']}** **Data entry:** {normalize_text(row.get('Data entry',''))} **Motivation** {normalize_text(row.get('Motivation',''))} **Guide** {normalize_text(row.get('Guide',''))} """ 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" # ========================= # PDF REPORT # ========================= 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_score = 0 for idx in sorted(state["results"].keys()): r = state["results"][idx] total_score += r["score"] story.append(Paragraph( f"{r['id']}. {r['name']}", styles["Heading2"] )) story.append(Spacer(1, 6)) if r.get("ref_image") and os.path.exists(r["ref_image"]): story.append(Paragraph("Reference image", styles["Normal"])) story.append(RLImage(r["ref_image"], width=8*cm, height=6*cm)) story.append(Spacer(1, 6)) if r.get("user_image") and os.path.exists(r["user_image"]): story.append(Paragraph("User photo", styles["Normal"])) story.append(RLImage(r["user_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(Spacer(1, 6)) story.append(Paragraph( f"Motivation:
{r['motivation']}", styles["Normal"] )) story.append(Spacer(1, 6)) if r.get("comment"): story.append(Paragraph( f"User observation:
{r['comment']}", styles["Normal"] )) story.append(Spacer(1, 12)) story.append(PageBreak()) story.append(Paragraph("Overall Result", styles["Heading1"])) story.append(Spacer(1, 12)) story.append(Paragraph(f"Total score: {total_score} / 48", styles["Normal"])) story.append(Paragraph( f"Soil quality classification: {soil_quality_label(total_score)}", styles["Normal"] )) story.append(Spacer(1, 12)) story.append(Paragraph( "Score interpretation:
" "• 15 – 25 → Low soil quality
" "• 26 – 37 → Medium soil quality
" "• 38 – 48 → High soil quality", styles["Normal"] )) story.append(PageBreak()) story.append(Paragraph("Credits & Acknowledgements", styles["Heading1"])) story.append(Spacer(1, 12)) story.append(Paragraph( """ This assessment tool is based on the guide:

Luján Soto, Raquel (2018).
Cuaderno de campo para la evaluación de prácticas regenerativas sobre la calidad del suelo.
Instituto de Sociología y Estudios Campesinos (Universidad de Córdoba)
Centro de Edafología y Biología Aplicada del Segura (CEBAS–CSIC).

The guide forms part of the doctoral research project “Investigación Acción Participativa en Agricultura Regenerativa”, supported by Fundación Bancaria “la Caixa” (Grant LCF/BQ/ES17/11600008).

This report and application are also inspired by the training programme on regenerative agriculture and ecological restoration developed by Ecorestauración. """, styles["Normal"] )) doc.build(story) tmp.close() return tmp.name # ========================= # STATE & NAVIGATION # ========================= def load_indicator(state): idx = state["idx"] row = state["indicators"][idx] saved = state["results"].get(idx, {}) indicator_id = int(row["Indicator ID"]) ref_image = get_indicator_image(indicator_id) data_entry = normalize_text(row.get("Data entry", "")).lower() return ( indicator_prompt(row, idx, len(state["indicators"])), ref_image, saved.get("assessment", "Medium"), saved.get("number"), saved.get("comment", ""), state ) def save_current(image, number, text, assessment, state): row = state["indicators"][state["idx"]] indicator_id = int(row["Indicator ID"]) state["results"][state["idx"]] = { "id": indicator_id, "name": normalize_text(row["Indicator Name"]), "assessment": assessment, "score": SCORE_MAP[assessment], "number": number, "comment": text, "motivation": normalize_text(row.get("Motivation", "")), "ref_image": get_indicator_image(indicator_id), "user_image": save_user_image(image), } def start_session(season, location): indicators = build_season_indicator_list(season) state = { "season": season, "location": location, "idx": 0, "indicators": indicators, "results": {} } return (*load_indicator(state), gr.update(visible=True), gr.update(visible=False)) def go_next(image, number, text, assessment, state): save_current(image, number, text, assessment, state) if state["idx"] < len(state["indicators"]) - 1: state["idx"] += 1 return (*load_indicator(state), gr.update(visible=True), gr.update(visible=False)) pdf = generate_pdf_report(state) return ( "✅ Assessment completed.", None, None, None, None, state, gr.update(visible=False), gr.update(value=pdf, visible=True) ) def go_prev(state): if state["idx"] > 0: state["idx"] -= 1 return (*load_indicator(state), state) # ========================= # UI # ========================= with gr.Blocks(title="Soil Quality Assessment") as demo: gr.Markdown("# 🌱 Soil Quality Assessment") state = gr.State({}) season = gr.Dropdown(["Fall", "Winter", "Spring", "Summer"], value="Spring") location = gr.Textbox(label="Location") start_btn = gr.Button("Start assessment") indicator_md = gr.Markdown() reference_image = gr.Image(label="Reference image", interactive=False) image_in = gr.Image(type="pil", label="Field photo") number_in = gr.Number(label="Numeric value") text_in = gr.Textbox(label="Observation") chosen = gr.Radio(["Low", "Medium", "High"], value="Medium") with gr.Row(): prev_btn = gr.Button("⬅ Previous") next_btn = gr.Button("Next ➡", visible=False) pdf_out = gr.File(label="📄 Download assessment report", visible=False) start_btn.click( start_session, inputs=[season, location], outputs=[indicator_md, reference_image, chosen, number_in, text_in, state, next_btn, pdf_out] ) next_btn.click( go_next, inputs=[image_in, number_in, text_in, chosen, state], outputs=[indicator_md, reference_image, chosen, number_in, text_in, state, next_btn, pdf_out] ) prev_btn.click( go_prev, inputs=[state], outputs=[indicator_md, reference_image, chosen, number_in, text_in, state] ) gr.Markdown( """ --- ### 📚 Credits & Acknowledgements This application is based on the methodological framework developed in: **Luján Soto, Raquel (2018)** *Cuaderno de campo para la evaluación de prácticas regenerativas sobre la calidad del suelo.* Instituto de Sociología y Estudios Campesinos (Universidad de Córdoba) Centro de Edafología y Biología Aplicada del Segura (CEBAS–CSIC) The guide was developed within the doctoral research project *“Investigación Acción Participativa en Agricultura Regenerativa”*, supported by Fundación Bancaria “la Caixa” (Grant **LCF/BQ/ES17/11600008**). This tool is also inspired by the educational approach and materials of **Ecorestauración** https://ecorestauracion.es/curso/ """ ) demo.launch()