Guarden / TECHNICAL_DOCUMENT.md
Crocolil's picture
Revert health check to share the advisor's LLM client
a3ee5be
|
Raw
History Blame Contribute Delete
17.6 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

Guarden β€” Technical Documentation

This document describes the architecture, data model, and machine-learning components behind Guarden, a Gradio application that helps users identify plants, track a virtual garden, and receive weather-aware watering, care recommendations, health checks...


1. High-level architecture

Guarden is a single-process Gradio app (app.py, ~950 lines) backed by a small set of pure-Python modules. There is no database server: each user gets a private, file-based "garden" stored on disk, and three external AI/ML models are called on demand via Hugging Face.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          Browser (Gradio UI)                         β”‚
β”‚  Location gate β†’ Garden board (drag & drop) β†’ Sidebar (watering /    β”‚
β”‚  forecast / assistant) β†’ Add-plant drawer                            β”‚
└───────────────┬────────────────────────────────────────────────────-β”˜
                 β”‚ Gradio Blocks events (click / change / .then chains)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€-┐
β”‚                              app.py                                   β”‚
β”‚  β€’ Per-user routing (BrowserState user_id β†’ user_data/<uuid>/)       β”‚
β”‚  β€’ Garden CRUD (load/save garden.json, photos, background, links)    β”‚
β”‚  β€’ Board rendering (HTML + SVG overlay + JS drag/drop bridge)         β”‚
β”‚  β€’ Orchestrates calls into modules/* and external APIs                β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚             β”‚               β”‚               β”‚
    β–Ό             β–Ό               β–Ό               β–Ό
modules/      modules/        modules/         modules/
classifier.py recommender.py  watering.py       advisor.py
(SigLIP ML     plant.py        weather_utils.py (HF Inference:
 image          (CSV lookup)   (Open-Meteo)       chat LLM +
 classifier)                                      vision LLM)
                                   β”‚
                                   β–Ό
                          utils/geo.py (Open-Meteo
                                geocoding)

Per-user data layout

user_data/
└── <uuid>/                      # one folder per browser, via gr.BrowserState
    β”œβ”€β”€ garden.json              # list of plant dicts (see Β§3)
    β”œβ”€β”€ background.jpg           # optional custom board background
    └── plant_photos/
        └── <plant_id>.jpg       # uploaded photo for each plant

A user_id (UUID4) is generated on first visit and persisted in the browser's local storage via gr.BrowserState, so the same browser always maps back to the same user_data/<uuid>/ folder β€” no login/auth required.


2. Tech stack

Layer Technology
UI / app framework Gradio 6.18 (Blocks API), custom CSS theme (static/style.css), small vanilla-JS bridge for the drag-and-drop board (BOARD_JS in app.py)
Plant genus classification Fine-tuned SigLIP vision transformer (transformers, local inference, CPU/GPU)
Gardening chat advisor LLM via Hugging Face Inference Providers (huggingface_hub.InferenceClient)
Photo health diagnostic Vision-language model via Hugging Face Inference Providers (multimodal chat)
Care metadata CSV lookup table (data/growth_csv/growth_ds.csv), pandas
Weather & geocoding Open-Meteo REST APIs (forecast, archive, geocoding) β€” no API key needed
Persistence Flat files: garden.json (JSON) + JPEG photos, per user, on local disk
Sprites Procedurally generated pixel-art PNGs (modules/pixel_art.py, pure PIL, no ML)

Runtime dependencies are pinned in requirements.txt. Training-only dependencies (datasets, accelerate, torchvision) live alongside the inference deps because the classifier's training script ships in the same repo (see Β§4.1).


3. Data model

Each plant in garden.json is a dict with the following fields (collected from how app.py reads/writes them):

{
  "id": "20260610_211240_094893",        // timestamp-based unique id
  "nickname": "Living Room Ficus",       // user-given name
  "photo": "user_data/<uid>/plant_photos/<id>.jpg",
  "genus": "Ficus",                      // predicted by the classifier
  "confidence": 92.4,                    // classifier confidence, %
  "added": "2026-06-10",
  "last_watered": "2026-06-12",          // ISO date or null
  "watering_history": ["2026-06-01", "2026-06-12"], // append-only log
  "rained": false,                       // true if last_watered was inferred from rain
  "watering_frequency_days": "Regular watering",   // raw CSV text
  "sunlight": "full sunlight",
  "soil": "sandy",
  "fertilization_type": "Balanced",
  "notes": "Ficus needs full sunlight. It thrives in sandy soil. ...",
  "position": { "x": 30.0, "y": 40.0 }, // % position on the garden board
  "neighbors": ["<other plant id>"],     // hand-drawn "neighbor" links
  "health": "Healthy β€” leaves look ..."  // last VLM health diagnosis, if any
}

This structure is the single source of truth: the board, the detail card, the watering table, the advisor and the health diagnostic all read/write this same list of dicts via load_garden(user_id) / save_garden(...).


4. Machine-learning components

Guarden uses three distinct AI models, each chosen for a different job: a small fine-tuned vision classifier for genus recognition (fast, local, deterministic), and two Hugging Face Inference-hosted generative models for natural-language and vision-language reasoning (advisor + health check).

4.1 Plant genus classifier (modules/classifier.py)

Task: given a photo of a plant, predict its botanical genus (e.g. Ficus, Aloe, Begonia) out of 289 genus classes.

Model: a fine-tuned google/siglip-base-patch16-224 (SigLIP β€” a CLIP-style vision transformer, ViT-B/16, 224Γ—224 input, 768-d hidden size) with a SiglipForImageClassification head (289-way softmax). The fine-tuned weights are pushed to a private HF Hub repo (Crocolil/HackatonSmall-storage) and exported as a clean config.json / model.safetensors / preprocessor_config.json bundle (~372 MB) under training/clean_export/.

Training pipeline (training/train_classifier.py):

  • Loads a datasets.DatasetDict (train/test split) of labelled plant photos, with one ClassLabel per genus (data/hf_plant_dataset/).
  • Builds id2label / label2id from the dataset's ClassLabel feature.
  • Data augmentation (train split): RandomResizedCrop(scale=0.8–1.0), RandomHorizontalFlip, ColorJitter(brightness/contrast/saturation=0.1), then ToTensor + SigLIP's own image-mean/std normalization.
  • Eval split: deterministic Resize + CenterCrop + normalize.
  • Fine-tuned end-to-end with πŸ€— Trainer / TrainingArguments:
    • num_train_epochs=3, per_device_*_batch_size=32, lr=5e-5, seed=42
    • bf16=True when CUDA is available
    • eval_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, metric_for_best_model="accuracy"
    • Metrics: top-1 accuracy and top-5 accuracy (compute_metrics compares argmax / top-5 logits vs. labels).
    • Optional --push-to-hub to publish the checkpoint to a private repo.

Inference (modules/classifier.py):

  • CLASSIFIER_MODEL_ID env var points to the Hub repo of the fine-tuned model (loaded lazily, cached as module-level globals).
  • classify_plant(image):
    1. AutoImageProcessor resizes/normalizes the uploaded PIL.Image.
    2. AutoModelForImageClassification runs a forward pass (torch.no_grad()).
    3. Softmax over the 289 logits β†’ (genus_name, confidence).
  • Called from app.py's add_plants_to_garden() for every uploaded photo; the predicted genus drives everything downstream (care metadata, sprite archetype, advisor context).

4.2 Care recommendation engine (modules/plant.py, modules/recommender.py)

Not a learned model β€” a deterministic lookup + template layer that turns the classifier's genus output into actionable care info:

  • Plant(genus) looks up data/growth_csv/growth_ds.csv (296 genus β†’ care-profile rows, derived from a public plants-growth dataset) for Watering, Sunlight, Soil, Fertilization Type.
  • If the genus isn't in the CSV (e.g. a class the classifier knows but the growth table doesn't cover), get_plant_info() falls back to generic defaults ("Water when soil is dry", "indirect sunlight", "well-drained", "No" fertilizer).
  • generate_care_notes() assembles a short natural-language note from these fields via string templates (no model call) β€” shown on the plant detail card under "Notes".

4.3 Watering scheduler (modules/watering.py + modules/weather_utils.py)

Also rule-based, but weather-aware:

  • _parse_watering_frequency() maps the CSV's free-text watering instructions (e.g. "Keep soil consistently moist", "Water weekly", "every 10 days") to an integer interval in days, via an exact-match table plus regex fallbacks (DEFAULT_INTERVAL = 4 days if nothing matches).
  • should_water(plant, last_watered, date, lat, lon) returns True if:
    • next_watering_date = last_watered + frequency_days has passed, and
    • did_or_will_rain(date, lat, lon, threshold=50%) is False β€” i.e. it didn't rain in the past (for historical dates) and isn't forecast to rain β‰₯50% (for today/future), so the app doesn't tell you to water a plant that nature is about to water for you.
  • load_garden() also back-fills last_watered from last_rained_date() on every load: if it rained more recently than the recorded watering date, the plant is considered watered by rain (rained: true), avoiding over-watering recommendations after a period of inactivity.
  • The sidebar's "Watering today" table is produced by get_watering_recommendations(), which runs should_water() for every plant against the live 7-day forecast.

4.4 AI gardening advisor β€” chat (modules/advisor.py::ask_about_plant)

Task: free-form Q&A about a specific plant ("Why are the leaves turning yellow?", "Can I plant this next to my tomatoes?").

  • Model: ADVISOR_MODEL_ID (default Qwen/Qwen2.5-Coder-3B-Instruct) served via Hugging Face Inference Providers (provider="nscale" by default), through huggingface_hub.InferenceClient.chat_completion.
  • Grounding / prompt construction (_build_system_prompt): the system prompt is dynamically built from the plant's care profile (sunlight, soil, watering frequency, fertilization) and its live watering status (computed via _watering_status() from last_watered), so the model knows whether the plant is overdue or recently watered before answering. If the user has drawn "neighbor" links on the board, the linked plants' name/genus are injected too, so the model can reason about companion-planting effects (shared pests, competition for light/water, beneficial pairings).
  • The model is instructed to answer in 2–4 sentences, in the same language as the question, and to never recommend toxic/dangerous substances.
  • On any InferenceClient error, the function logs [advisor] HF Inference error: ... and returns a friendly fallback message instead of crashing the UI.
  • Wired in app.py (ask_plant_advisor) to the "πŸ€– Ask the assistant" button in the sidebar's Plant assistant panel, which only appears once a plant is selected on the board.

4.5 Photo-based health diagnostic β€” vision-language (modules/advisor.py::diagnose_plant_health)

Task: given a new photo of the selected plant, assess its health (leaves, stems, soil) and store the verdict on the plant record.

  • Model: the same ADVISOR_MODEL_ID / ADVISOR_PROVIDER client as the chat advisor (Β§4.4), again via InferenceClient.chat_completion β€” but this time with a multimodal message: the uploaded PIL.Image is re-encoded as JPEG, base64-encoded, and sent as an OpenAI-style content array ({"type": "text", ...} + {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}).
  • The prompt asks the model to start its reply with exactly one status word β€” Healthy, Needs attention, or Sick β€” followed by a 1–3 sentence explanation and a suggested action.
  • app.py::diagnose_selected_plant_health persists the raw model response into plant["health"], which is then surfaced on the plant detail card (🩺 Health: ...) every time the garden is reloaded β€” so the diagnosis survives page refreshes and is visible alongside the watering history.
  • Same defensive error handling as Β§4.4 ([advisor] HF Inference health-check error: ... + fallback message).

4.6 Procedural pixel-art sprites (modules/pixel_art.py) β€” not ML, but genus-aware

Worth a short mention because it feels like generative output but is fully deterministic: each genus is mapped to one of 6 hand-authored 16Γ—16 "plant archetype" sprites (cactus, succulent, fern, flower, palm, trailing) and one of 4 pot styles, based on the genus's Growth / Soil / Sunlight values from the same growth_ds.csv (e.g. sandy soil + full sun + slow growth β†’ cactus in a terracotta pot). Genera missing from the CSV get a stable hash-based archetype/pot assignment so the same unknown genus always renders the same sprite. Sprites are rendered once with PIL nearest-neighbour upscaling and cached to static/sprites/<genus>.png.


5. External APIs

All weather/geocoding calls go to Open-Meteo (free, no API key):

Function Endpoint Used for
utils.geo.city_to_coordinates geocoding-api.open-meteo.com/v1/search Turn the user's city into (lat, lon) at the location gate
modules.weather_utils.weather_values api.open-meteo.com/v1/forecast (16-day daily) 7-day forecast table (conditions, temp, rain %, wind)
modules.weather_utils.did_or_will_rain forecast (future) or archive-api.open-meteo.com (past) Decide whether a plant should be watered today / was watered by rain
modules.weather_utils.last_rained_date archive-api.open-meteo.com/v1/archive (15-day lookback) Back-fill last_watered on garden load

weather_comment() maps Open-Meteo's numeric WMO weather codes to short emoji + text labels (e.g. 80 β†’ "🌦️ Slight rain showers") shown in the forecast table.


6. UI / front-end notes

  • Single gr.Blocks app, themed with gr.themes.Soft() plus a large custom stylesheet (static/style.css) that overrides Gradio's CSS variables for a green "Guarden" theme (custom button gradients, card radii, etc.).
  • Garden board: plants are rendered as absolutely-positioned <div> sprites inside get_garden_board_html(). A small injected <script> (BOARD_JS) uses pointer events to support:
    • Drag & drop β†’ updates position: {x%, y%} (hidden gr.Number + sync button bridge the JS β†’ Python boundary).
    • Click to select β†’ opens the detail card + action row + assistant panel for that plant.
    • "πŸ”— Link Neighbours" mode β†’ click two sprites to toggle a neighbors link, drawn as a dashed SVG line between them (re-rendered on every board update, so links follow plants when dragged).
  • Sidebar: watering recommendations table, 7-day forecast table (custom CSS turns the Gradio dataframe into card-style rows with a styled header row), and the Plant assistant panel (chat + health diagnostic), which only becomes visible once a plant is selected.
  • Per-user custom background: an uploaded image is saved as user_data/<uid>/background.jpg and applied as the board's background-image via inline CSS.

7. Deployment

The app is shipped as a Hugging Face Space (sdk: gradio, sdk_version: 6.18.0, entry point app.py, see the README.md front matter). Configuration is entirely via environment variables, with sane defaults baked in so the app runs locally without any secrets:

Env var Default Purpose
WEATHER_CITY "Marseille" Initial forecast location before the user sets one
CLASSIFIER_MODEL_ID "your-username/plant-genus-classifier" HF Hub repo of the fine-tuned SigLIP genus classifier
ADVISOR_MODEL_ID / ADVISOR_PROVIDER Qwen/Qwen2.5-Coder-3B-Instruct / nscale Chat advisor + health-diagnostic model and HF Inference provider (shared)
HF_TOKEN β€” Hugging Face token for Inference Providers (advisor + health check)

app.launch(allowed_paths=[...]) whitelists user_data/, static/ and plant_photos/ so per-user photos, sprites and backgrounds can be served back to the browser via Gradio's /gradio_api/file= route.