Spaces:
Sleeping
Sleeping
| """ | |
| 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("<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_score = 0 | |
| for idx in sorted(state["results"].keys()): | |
| r = state["results"][idx] | |
| total_score += r["score"] | |
| story.append(Paragraph( | |
| f"<b>{r['id']}. {r['name']}</b>", | |
| styles["Heading2"] | |
| )) | |
| story.append(Spacer(1, 6)) | |
| if r.get("ref_image") and os.path.exists(r["ref_image"]): | |
| story.append(Paragraph("<i>Reference image</i>", 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("<i>User photo</i>", styles["Normal"])) | |
| story.append(RLImage(r["user_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(Spacer(1, 6)) | |
| story.append(Paragraph( | |
| f"<b>Motivation:</b><br/>{r['motivation']}", | |
| styles["Normal"] | |
| )) | |
| story.append(Spacer(1, 6)) | |
| if r.get("comment"): | |
| story.append(Paragraph( | |
| f"<b>User observation:</b><br/>{r['comment']}", | |
| styles["Normal"] | |
| )) | |
| story.append(Spacer(1, 12)) | |
| story.append(PageBreak()) | |
| story.append(Paragraph("<b>Overall Result</b>", styles["Heading1"])) | |
| story.append(Spacer(1, 12)) | |
| story.append(Paragraph(f"<b>Total score:</b> {total_score} / 48", styles["Normal"])) | |
| story.append(Paragraph( | |
| f"<b>Soil quality classification:</b> {soil_quality_label(total_score)}", | |
| styles["Normal"] | |
| )) | |
| story.append(Spacer(1, 12)) | |
| story.append(Paragraph( | |
| "<i>Score interpretation:</i><br/>" | |
| "• 15 – 25 → Low soil quality<br/>" | |
| "• 26 – 37 → Medium soil quality<br/>" | |
| "• 38 – 48 → High soil quality", | |
| styles["Normal"] | |
| )) | |
| story.append(PageBreak()) | |
| story.append(Paragraph("<b>Credits & Acknowledgements</b>", styles["Heading1"])) | |
| story.append(Spacer(1, 12)) | |
| story.append(Paragraph( | |
| """ | |
| This assessment tool is based on the guide:<br/><br/> | |
| <b>Luján Soto, Raquel (2018).</b><br/> | |
| <i>Cuaderno de campo para la evaluación de prácticas regenerativas sobre la calidad del suelo.</i><br/> | |
| Instituto de Sociología y Estudios Campesinos (Universidad de Córdoba)<br/> | |
| Centro de Edafología y Biología Aplicada del Segura (CEBAS–CSIC).<br/><br/> | |
| The guide forms part of the doctoral research project | |
| <i>“Investigación Acción Participativa en Agricultura Regenerativa”</i>, | |
| supported by Fundación Bancaria “la Caixa” | |
| (Grant LCF/BQ/ES17/11600008).<br/><br/> | |
| This report and application are also inspired by the training programme on | |
| regenerative agriculture and ecological restoration developed by | |
| <b>Ecorestauración</b>. | |
| """, | |
| 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() | |