Instructions to use nadjla12/skyline-api with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Keras
How to use nadjla12/skyline-api with Keras:
# Available backend options are: "jax", "torch", "tensorflow". import os os.environ["KERAS_BACKEND"] = "jax" import keras model = keras.saving.load_model("hf://nadjla12/skyline-api") - Notebooks
- Google Colab
- Kaggle
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # SKYLINE LAND โ main.py (CNN + VLM โ ูุณุฎุฉ ู ูุญุฏุฉ ุขู ูุฉ) | |
| # Admin โ POST /predict | |
| # User โ POST /predict + POST /vlm-analyze + POST /vlm-chat | |
| # ููุงูู ุง ูุณุชุฎุฏู ุงู ููุณ ุฌุฏูู diagnostics ูู Supabase | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| import os, json, io, uuid, cv2, base64 | |
| import numpy as np | |
| from datetime import datetime | |
| from dotenv import load_dotenv | |
| from typing import Optional | |
| import tensorflow as tf | |
| import matplotlib | |
| matplotlib.use('Agg') | |
| import matplotlib.pyplot as plt | |
| from PIL import Image | |
| from fastapi import FastAPI, File, UploadFile, HTTPException, Form, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse | |
| from supabase import create_client, Client | |
| load_dotenv() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # SUPABASE | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| SUPABASE_URL = os.getenv("SUPABASE_URL") | |
| SUPABASE_SERVICE_KEY = os.getenv("SUPABASE_SERVICE_KEY") | |
| TABLE_NAME = os.getenv("TABLE_NAME", "diagnostics") | |
| GRADCAM_BUCKET = os.getenv("GRADCAM_BUCKET", "gradcam-results") | |
| MODEL_PATH = os.getenv("MODEL_PATH", "./models/skyline_savedmodel") | |
| EXPERT_DB_PATH = os.getenv("EXPERT_DB_PATH", "./models/expert_db.json") | |
| supabase: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_KEY) | |
| print("โ Supabase client initialized.") | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # GEMINI VLM โ ุขู ู: ูุง ููุณุฑ ุงูู app ุฅุฐุง ุงูู ูุชุงุญ ุบูุฑ ู ูุฌูุฏ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "") | |
| GEMINI_READY = False | |
| gemini_model = None | |
| if GEMINI_API_KEY: | |
| try: | |
| import google.generativeai as genai | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| gemini_model = genai.GenerativeModel("gemini-pro") | |
| GEMINI_READY = True | |
| print("โ Gemini VLM initialized.") | |
| except Exception as e: | |
| print(f"โ ๏ธ Gemini init failed (VLM disabled): {e}") | |
| else: | |
| print("โ ๏ธ GEMINI_API_KEY not set โ VLM endpoints will return fallback from CNN.") | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # CNN MODEL | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| print("โณ Loading CNN model...") | |
| _loaded = tf.saved_model.load(MODEL_PATH) | |
| _infer = _loaded.signatures['serving_default'] | |
| INPUT_KEY = 'input_layer_7' | |
| OUTPUT_KEY = None | |
| def _predict_raw(img_input: tf.Tensor) -> tf.Tensor: | |
| global OUTPUT_KEY | |
| result = _infer(**{INPUT_KEY: img_input}) | |
| if OUTPUT_KEY is None: | |
| OUTPUT_KEY = list(result.keys())[0] | |
| return result[OUTPUT_KEY] | |
| _test = tf.zeros((1, 224, 224, 3), dtype=tf.float32) | |
| _out = _predict_raw(_test) | |
| print(f"โ CNN loaded โ output shape: {_out.shape}") | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # EXPERT DB | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with open(EXPERT_DB_PATH, 'r', encoding='utf-8') as f: | |
| expert_db = json.load(f) | |
| CLASS_NAMES = list(expert_db.keys()) | |
| HEALTHY_CLASSES = {k for k in CLASS_NAMES if 'healthy' in k.lower()} | |
| print(f"โ Expert DB: {len(CLASS_NAMES)} classes | {len(HEALTHY_CLASSES)} healthy") | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # GRAD-CAM | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def get_gradcam_and_visualizations(img_array: np.ndarray): | |
| img_float = img_array.astype(np.float32) | |
| inp = tf.keras.applications.efficientnet.preprocess_input(img_float[np.newaxis]) | |
| inp_var = tf.Variable(inp, trainable=True, dtype=tf.float32) | |
| with tf.GradientTape() as tape: | |
| tape.watch(inp_var) | |
| preds = _predict_raw(inp_var) | |
| pred_idx = int(tf.argmax(preds[0])) | |
| score = preds[:, pred_idx] | |
| grads = tape.gradient(score, inp_var) | |
| heatmap = tf.reduce_mean(tf.abs(grads[0]), axis=-1).numpy() | |
| if heatmap.max() > 0: | |
| heatmap = heatmap / heatmap.max() | |
| confidence = float(preds[0][pred_idx]) | |
| # Original โ base64 | |
| orig_buf = io.BytesIO() | |
| Image.fromarray(img_array.astype(np.uint8)).save(orig_buf, format='PNG') | |
| original_b64 = base64.b64encode(orig_buf.getvalue()).decode() | |
| # Heatmap โ base64 | |
| hm_resized = cv2.resize(heatmap, (224, 224)) | |
| hm_colored = (plt.cm.jet(hm_resized)[:, :, :3] * 255).astype(np.uint8) | |
| hm_buf = io.BytesIO() | |
| Image.fromarray(hm_colored).save(hm_buf, format='PNG') | |
| heatmap_b64 = base64.b64encode(hm_buf.getvalue()).decode() | |
| # Overlay โ base64 | |
| overlay = np.clip(hm_colored * 0.55 + img_array * 0.45, 0, 255).astype(np.uint8) | |
| ov_buf = io.BytesIO() | |
| Image.fromarray(overlay).save(ov_buf, format='PNG') | |
| overlay_b64 = base64.b64encode(ov_buf.getvalue()).decode() | |
| return heatmap, pred_idx, confidence, original_b64, heatmap_b64, overlay_b64 | |
| def build_gradcam_png_bytes(heatmap_np, img_array, is_healthy) -> bytes: | |
| fig, axes = plt.subplots(1, 3, figsize=(15, 5), facecolor='white') | |
| for ax in axes: ax.axis('off') | |
| axes[0].imshow(img_array.astype(np.uint8)); axes[0].set_title('Original') | |
| if is_healthy: | |
| axes[1].imshow(img_array.astype(np.uint8)); axes[1].set_title('Healthy') | |
| axes[2].imshow(img_array.astype(np.uint8)); axes[2].set_title('Healthy') | |
| else: | |
| hm = cv2.resize(heatmap_np, (224, 224)) | |
| sup = np.clip(plt.cm.jet(hm)[:, :, :3] * 0.55 + img_array / 255.0 * 0.45, 0, 1) | |
| axes[1].imshow(hm, cmap='jet'); axes[1].set_title('Grad-CAM') | |
| axes[2].imshow(sup); axes[2].set_title('Overlay') | |
| buf = io.BytesIO() | |
| plt.tight_layout(); fig.savefig(buf, format='png', dpi=150, facecolor='white') | |
| plt.close(fig); buf.seek(0) | |
| return buf.read() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # STORAGE | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def upload_gradcam_to_storage(png_bytes: bytes, filename: str) -> Optional[str]: | |
| try: | |
| path = f"overlays/{filename}" | |
| supabase.storage.from_(GRADCAM_BUCKET).upload( | |
| path=path, file=png_bytes, | |
| file_options={"content-type": "image/png", "upsert": "true"} | |
| ) | |
| url = supabase.storage.from_(GRADCAM_BUCKET).get_public_url(path) | |
| print(f"โ Storage: {path}") | |
| return url | |
| except Exception as e: | |
| print(f"โ ๏ธ Storage upload failed: {e}") | |
| return None | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # HELPER โ ุญูุธ ูู Supabase (ู ุดุชุฑู ุจูู Admin ู User) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def save_diagnosis(record: dict) -> bool: | |
| try: | |
| result = supabase.table(TABLE_NAME).insert(record).execute() | |
| if result.data: | |
| print(f"โ DB saved: {record.get('class_key')} | user={record.get('profile_id')} | conf={record.get('confidence')}%") | |
| return True | |
| print(f"โ ๏ธ Insert returned no data") | |
| return False | |
| except Exception as e: | |
| print(f"โ ๏ธ DB insert failed: {e}") | |
| return False | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # FASTAPI APP | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| app = FastAPI(title="Skyline LAND API", version="3.0.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # โโ GET / โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def root(): | |
| return { | |
| "status": "Skyline LAND API โ ", | |
| "cnn_classes": len(CLASS_NAMES), | |
| "vlm_ready": GEMINI_READY, | |
| "version": "3.0.0", | |
| } | |
| # โโ GET /health โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def health(): | |
| db_ok = False | |
| try: | |
| supabase.table(TABLE_NAME).select("id").limit(1).execute() | |
| db_ok = True | |
| except: pass | |
| return { | |
| "api": "ok", | |
| "db": "ok" if db_ok else "error", | |
| "cnn": f"{len(CLASS_NAMES)} classes", | |
| "vlm": "ready" if GEMINI_READY else "disabled (no API key)", | |
| "bucket": GRADCAM_BUCKET, | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # POST /predict โ Admin + User (CNN + Grad-CAM) | |
| # ููุง ุงูุตูุญุชูู ุชุณุชุฏุนู ูุฐุง ุงูู endpoint | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def predict( | |
| file: UploadFile = File(...), | |
| user_id: Optional[str] = Form(None), | |
| ): | |
| if not file.content_type.startswith("image/"): | |
| raise HTTPException(400, "File must be an image") | |
| raw = await file.read() | |
| try: | |
| pil_img = Image.open(io.BytesIO(raw)).convert("RGB").resize((224, 224)) | |
| img_array = np.array(pil_img, dtype=np.uint8) | |
| except Exception as e: | |
| raise HTTPException(400, f"Cannot open image: {e}") | |
| # CNN + Grad-CAM | |
| heatmap_np, class_idx, conf_raw, original_b64, heatmap_b64, overlay_b64 = \ | |
| get_gradcam_and_visualizations(img_array) | |
| conf = round(conf_raw * 100, 2) | |
| class_key = CLASS_NAMES[class_idx] if class_idx < len(CLASS_NAMES) else f"class_{class_idx}" | |
| info = expert_db.get(class_key, {}) | |
| is_healthy = class_key in HEALTHY_CLASSES | |
| # Top-3 | |
| inp_np = tf.keras.applications.efficientnet.preprocess_input(img_array.astype(np.float32)[np.newaxis]) | |
| all_preds = _predict_raw(inp_np)[0].numpy() | |
| top3_idx = np.argsort(all_preds)[-3:][::-1] | |
| top3 = [ | |
| { | |
| "class": CLASS_NAMES[i], | |
| "name_en": expert_db.get(CLASS_NAMES[i], {}).get("en", CLASS_NAMES[i]), | |
| "conf": round(float(all_preds[i]) * 100, 2), | |
| } | |
| for i in top3_idx | |
| ] | |
| # Storage upload (ููุท ูููุจุงุช ุงูู ุฑูุถ) | |
| gradcam_url = None | |
| if not is_healthy: | |
| gradcam_png = build_gradcam_png_bytes(heatmap_np, img_array, is_healthy) | |
| gradcam_url = upload_gradcam_to_storage(gradcam_png, f"{uuid.uuid4()}.png") | |
| # ุญูุธ ูู Supabase โ ููุณ ุงูุฌุฏูู ููู Admin ูุงูู User | |
| record = { | |
| "profile_id": user_id, | |
| "class_key": class_key, | |
| "disease_name_en": info.get("en", class_key), | |
| "disease_name_ar": info.get("ar", ""), | |
| "advice_en": info.get("adv_en", ""), | |
| "advice_ar": info.get("adv_ar", ""), | |
| "treatment_en": info.get("chem_en", ""), | |
| "treatment_ar": info.get("chem_ar", ""), | |
| "infection_type": info.get("infection_type", None), | |
| "is_healthy": is_healthy, | |
| "confidence": conf, | |
| "gradcam_url": gradcam_url, | |
| "analyzed_at": datetime.utcnow().isoformat(), | |
| } | |
| saved_to_db = save_diagnosis(record) | |
| return JSONResponse({ | |
| "success": True, | |
| "saved_to_db": saved_to_db, | |
| "class_key": class_key, | |
| "disease_name_en": record["disease_name_en"], | |
| "disease_name_ar": record["disease_name_ar"], | |
| "confidence": conf, | |
| "badge": "High" if conf >= 90 else "Good" if conf >= 75 else "Medium" if conf >= 60 else "Low", | |
| "is_healthy": is_healthy, | |
| "advice_en": record["advice_en"], | |
| "advice_ar": record["advice_ar"], | |
| "treatment_en": record["treatment_en"], | |
| "treatment_ar": record["treatment_ar"], | |
| "infection_type": record["infection_type"], | |
| "gradcam_url": gradcam_url, | |
| "original_b64": original_b64, | |
| "heatmap_b64": heatmap_b64, | |
| "overlay_b64": overlay_b64, | |
| "top3": top3, | |
| }) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # POST /vlm-analyze โ User ููุท (ุชุญููู ุฃููู ุจู Gemini Vision) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def vlm_analyze( | |
| file: Optional[UploadFile] = File(None), | |
| lang: str = Form("en"), | |
| cnn_context: Optional[str] = Form(None), | |
| user_id: Optional[str] = Form(None), | |
| ): | |
| # โโ Fallback ุฅุฐุง Gemini ุบูุฑ ุฌุงูุฒ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if not GEMINI_READY or gemini_model is None: | |
| fallback = _build_fallback_analysis(cnn_context, lang) | |
| return JSONResponse({"success": True, "analysis": fallback, "source": "cnn_fallback"}) | |
| # โโ Prompt ุญุณุจ ุงููุบุฉ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if lang == "ar": | |
| prompt = """ุฃูุช ุฎุจูุฑ ุฒุฑุงุนู ู ุชุฎุตุต ูู ุฃู ุฑุงุถ ุงููุจุงุช ูุงูู ุญุงุตูู. | |
| ุญููู ุงูุตูุฑุฉ ููุฏูู ุชูุฑูุฑุงู ุดุงู ูุงู ูู ูุธู ุงู ูุดู ู: | |
| **1. ุงูุชุดุฎูุต:** ู ุง ุงูู ุฑุถ ุฃู ุงูุญุงูุฉ ุงูุตุญูุฉ ุงูุชู ุชุฑุงูุงุ | |
| **2. ุงูุฃุนุฑุงุถ ุงูู ุฑุฆูุฉ:** ุตู ู ุง ูุธูุฑ ูู ุงูุตูุฑุฉ ุจุฏูุฉ | |
| **3. ุงูุณุจุจ:** ุงูู ู ุฑุถ ุฃู ุงูุนุงู ู ุงูุจูุฆู ุงูู ุณุคูู | |
| **4. ุฏุฑุฌุฉ ุงูุฎุทูุฑุฉ:** ู ูุฎูุถุฉ / ู ุชูุณุทุฉ / ู ุฑุชูุนุฉ โ ููู ุงุฐุงุ | |
| **5. ุฎุทุฑ ุงูุงูุชุดุงุฑ:** ูู ูู ูู ุฃู ูุตูุจ ูุจุงุชุงุช ู ุฌุงูุฑุฉุ | |
| **6. ุงูุนูุงุฌ ุงูููุฑู:** ุฎุทูุงุช ุนู ููุฉ ูุจุฏุฃ ุจูุง ุงูู ุฒุงุฑุน ุงูุขู | |
| **7. ุงูููุงูุฉ:** ููู ูุชุฌูุจ ุชูุฑุงุฑ ุงูู ุดููุฉ ูู ุงูู ูุงุณู ุงููุงุฏู ุฉ | |
| ุฃุฌุจ ุจุงููุบุฉ ุงูุนุฑุจูุฉ ุงููุตุญู ุงูุจุณูุทุฉ ุงูู ูุงุณุจุฉ ููู ุฒุงุฑุน ุงูุฌุฒุงุฆุฑู.""" | |
| else: | |
| prompt = """You are an expert agricultural plant pathologist. | |
| Analyze this plant/field image and provide a structured comprehensive report: | |
| **1. Diagnosis:** What disease or health condition is present? | |
| **2. Visible Symptoms:** Describe exactly what appears in the image | |
| **3. Cause:** Pathogen or environmental factor responsible | |
| **4. Severity Level:** Low / Medium / High โ and why? | |
| **5. Spread Risk:** Can it infect neighboring plants? | |
| **6. Immediate Treatment:** Practical steps the farmer can take now | |
| **7. Prevention:** How to avoid recurrence in future seasons | |
| Be thorough, practical, and clear for a farming context.""" | |
| # ุฅุถุงูุฉ ุณูุงู CNN ุฅุฐุง ู ุชููุฑ | |
| if cnn_context: | |
| try: | |
| ctx = json.loads(cnn_context) | |
| cnn_info = ( | |
| f"\n\n[CNN Pre-detection: '{ctx.get('disease_name_en')}' " | |
| f"({ctx.get('confidence', 0):.1f}% confidence) โ " | |
| f"{'Healthy' if ctx.get('is_healthy') else 'Diseased'}]" | |
| ) | |
| prompt += cnn_info | |
| except: pass | |
| try: | |
| parts = [prompt] | |
| if file: | |
| raw = await file.read() | |
| parts.append({"mime_type": file.content_type or "image/jpeg", "data": raw}) | |
| response = gemini_model.generate_content(parts) | |
| analysis = response.text | |
| return JSONResponse({ | |
| "success": True, | |
| "analysis": analysis, | |
| "source": "gemini_vlm", | |
| }) | |
| except Exception as e: | |
| print(f"โ ๏ธ Gemini VLM error: {e}") | |
| fallback = _build_fallback_analysis(cnn_context, lang) | |
| return JSONResponse({"success": True, "analysis": fallback, "source": "cnn_fallback"}) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # POST /vlm-chat โ User ููุท (ุฏุฑุฏุดุฉ ู ุชุนุฏุฏุฉ ุงูุฃุฏูุงุฑ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def vlm_chat( | |
| message: str = Form(...), | |
| lang: str = Form("en"), | |
| history: Optional[str] = Form(None), | |
| file: Optional[UploadFile] = File(None), | |
| cnn_context: Optional[str] = Form(None), | |
| ): | |
| # โโ Fallback ุฅุฐุง Gemini ุบูุฑ ุฌุงูุฒ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if not GEMINI_READY or gemini_model is None: | |
| reply = _build_fallback_chat(message, cnn_context, lang) | |
| return JSONResponse({"success": True, "reply": reply, "source": "cnn_fallback"}) | |
| # โโ System prompt โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if lang == "ar": | |
| sys_prompt = "ุฃูุช ุฎุจูุฑ ุฒุฑุงุนู ู ุชุฎุตุต. ุฃุฌุจ ุจุงููุบุฉ ุงูุนุฑุจูุฉ ููุท ูุจุฃุณููุจ ุจุณูุท ู ูุงุณุจ ููู ุฒุงุฑุน." | |
| else: | |
| sys_prompt = "You are an expert agricultural advisor. Reply only in English, clearly and practically for a farmer." | |
| # ุฅุถุงูุฉ ุณูุงู CNN | |
| if cnn_context: | |
| try: | |
| ctx = json.loads(cnn_context) | |
| ctx_txt = ( | |
| f" CNN detected: '{ctx.get('disease_name_en')}' " | |
| f"({ctx.get('confidence', 0):.1f}% confidence)." | |
| ) | |
| sys_prompt += ctx_txt | |
| except: pass | |
| # ุจูุงุก ุชุงุฑูุฎ ุงูู ุญุงุฏุซุฉ | |
| gemini_history = [ | |
| {"role": "user", "parts": [sys_prompt]}, | |
| {"role": "model", "parts": ["Understood. I'm your agricultural expert assistant."]}, | |
| ] | |
| if history: | |
| try: | |
| hist = json.loads(history) | |
| for msg in hist[-6:]: # ุขุฎุฑ 6 ุฑุณุงุฆู ููุท ูุชูููุฑ ุงูุชููู | |
| role = "user" if msg["role"] == "user" else "model" | |
| gemini_history.append({"role": role, "parts": [msg["content"]]}) | |
| except: pass | |
| try: | |
| chat = gemini_model.start_chat(history=gemini_history) | |
| parts = [message] | |
| if file: | |
| raw = await file.read() | |
| parts.append({"mime_type": file.content_type or "image/jpeg", "data": raw}) | |
| response = chat.send_message(parts) | |
| return JSONResponse({ | |
| "success": True, | |
| "reply": response.text, | |
| "source": "gemini_vlm", | |
| }) | |
| except Exception as e: | |
| print(f"โ ๏ธ Gemini chat error: {e}") | |
| reply = _build_fallback_chat(message, cnn_context, lang) | |
| return JSONResponse({"success": True, "reply": reply, "source": "cnn_fallback"}) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # POST /chat โ ุฏุฑุฏุดุฉ ุจุณูุทุฉ (ู ุชูุงูู ู ุน HTML user) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def chat_simple(request: Request): | |
| try: | |
| body = await request.json() | |
| message = body.get("message", "") | |
| lang = body.get("lang", "en") | |
| history = body.get("history", []) | |
| initial_report = body.get("initial_report", "") | |
| # Fallback ุฅุฐุง Gemini ุบูุฑ ุฌุงูุฒ | |
| if not GEMINI_READY or gemini_model is None: | |
| return JSONResponse({ | |
| "success": True, | |
| "response": _build_simple_chat_fallback(message, initial_report, lang), | |
| "reply": _build_simple_chat_fallback(message, initial_report, lang), | |
| "source": "fallback" | |
| }) | |
| # ุจูุงุก ุงูุณูุงู | |
| context = "" | |
| if initial_report: | |
| context = f"Previous diagnosis report: {initial_report[:500]}\n\n" | |
| if lang == "ar": | |
| system_prompt = f"""ุฃูุช ุฎุจูุฑ ุฒุฑุงุนู ู ุชุฎุตุต ูู ุฃู ุฑุงุถ ุงููุจุงุช. | |
| {context} | |
| ุฃุฌุจ ุนูู ุณุคุงู ุงูู ุฒุงุฑุน ุจุงููุบุฉ ุงูุนุฑุจูุฉ ููุทุ ุจุฃุณููุจ ุจุณูุท ูุนู ูู. | |
| ูุฏู ูุตุงุฆุญ ูุงุถุญุฉ ุญูู ุงูุนูุงุฌ ูุงูููุงูุฉ.""" | |
| else: | |
| system_prompt = f"""You are an expert agricultural plant pathologist. | |
| {context} | |
| Answer the farmer's question in English only, in a clear and practical way. | |
| Provide specific advice on treatment and prevention.""" | |
| # ุจูุงุก ุชุงุฑูุฎ ุงูู ุญุงุฏุซุฉ | |
| gemini_history = [ | |
| {"role": "user", "parts": [system_prompt]}, | |
| {"role": "model", "parts": ["Understood. I'm your agricultural expert assistant. How can I help you today?"]}, | |
| ] | |
| for msg in history[-10:]: | |
| role = "user" if msg.get("role") == "user" else "model" | |
| gemini_history.append({"role": role, "parts": [msg.get("content", "")]}) | |
| try: | |
| chat = gemini_model.start_chat(history=gemini_history) | |
| response = chat.send_message(message) | |
| return JSONResponse({ | |
| "success": True, | |
| "response": response.text, | |
| "reply": response.text, | |
| "source": "gemini_vlm" | |
| }) | |
| except Exception as e: | |
| print(f"โ ๏ธ Gemini chat error: {e}") | |
| fallback = _build_simple_chat_fallback(message, initial_report, lang) | |
| return JSONResponse({ | |
| "success": True, | |
| "response": fallback, | |
| "reply": fallback, | |
| "source": "error_fallback" | |
| }) | |
| except Exception as e: | |
| print(f"โ ๏ธ Chat endpoint error: {e}") | |
| return JSONResponse({ | |
| "success": False, | |
| "response": "Chat service temporarily unavailable. Please try again later.", | |
| "reply": "Chat service temporarily unavailable. Please try again later.", | |
| "source": "error" | |
| }) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # GET /history โ Admin (ูู ุงูุจูุงูุงุช) + User (ุจูุงูุงุชู ููุท) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def history(profile_id: Optional[str] = None, limit: int = 20): | |
| try: | |
| query = supabase.table(TABLE_NAME).select( | |
| "id, profile_id, class_key, disease_name_en, disease_name_ar, " | |
| "confidence, is_healthy, gradcam_url, analyzed_at" | |
| ) | |
| if profile_id: | |
| query = query.eq("profile_id", profile_id) | |
| response = query.order("analyzed_at", desc=True).limit(limit).execute() | |
| return {"success": True, "count": len(response.data), "data": response.data} | |
| except Exception as e: | |
| raise HTTPException(500, f"DB error: {e}") | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # HELPERS โ Fallback ุจุฏูู Gemini (ูุณุชุฎุฏู ูุชูุฌุฉ CNN) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _build_fallback_analysis(cnn_context: Optional[str], lang: str) -> str: | |
| """ูุจูู ุชุญูููุงู ู ู ูุชูุฌุฉ CNN ุนูุฏู ุง ูููู Gemini ุบูุฑ ู ุชุงุญ""" | |
| if not cnn_context: | |
| return ("ูุง ูู ูู ุงูุงุชุตุงู ุจู Gemini VLM ุญุงููุงู. ูุฑุฌู ุงุณุชุฎุฏุงู ูุชูุฌุฉ CNN ุฃุนูุงู." | |
| if lang == "ar" else | |
| "Gemini VLM is not available. Please use the CNN result above.") | |
| try: | |
| ctx = json.loads(cnn_context) | |
| if lang == "ar": | |
| return ( | |
| f"**ุงูุชุดุฎูุต:** {ctx.get('disease_name_ar') or ctx.get('disease_name_en', 'โ')}\n\n" | |
| f"**ูุณุจุฉ ุงูุซูุฉ:** {ctx.get('confidence', 0):.1f}%\n\n" | |
| f"**ุงูุญุงูุฉ:** {'ูุจุงุช ุณููู โ ' if ctx.get('is_healthy') else 'ู ุตุงุจ โ ๏ธ'}\n\n" | |
| f"**ุงููุตูุญุฉ:** {ctx.get('advice_ar') or ctx.get('advice_en', 'ุฑุงุฌุน ุจุฑูุชูููู ุงูุนูุงุฌ.')}\n\n" | |
| f"**ุงูุนูุงุฌ:** {ctx.get('treatment_ar') or ctx.get('treatment_en', 'ุงุณุชุดุฑ ุฎุจูุฑุงู ุฒุฑุงุนูุงู.')}\n\n" | |
| f"_ู ูุงุญุธุฉ: ูุฐุง ุงูุชุญููู ู ุจูู ุนูู ูุชูุฌุฉ CNN. ูู ุจุฅุถุงูุฉ GEMINI_API_KEY ููุญุตูู ุนูู ุชุญููู VLM ูุงู ู._" | |
| ) | |
| else: | |
| return ( | |
| f"**Diagnosis:** {ctx.get('disease_name_en', 'โ')}\n\n" | |
| f"**Confidence:** {ctx.get('confidence', 0):.1f}%\n\n" | |
| f"**Status:** {'Healthy โ ' if ctx.get('is_healthy') else 'Diseased โ ๏ธ'}\n\n" | |
| f"**Advice:** {ctx.get('advice_en', 'Follow the treatment protocol.')}\n\n" | |
| f"**Treatment:** {ctx.get('treatment_en', 'Consult an agricultural expert.')}\n\n" | |
| f"_Note: This analysis is based on CNN result. Add GEMINI_API_KEY for full VLM analysis._" | |
| ) | |
| except: | |
| return "Analysis based on CNN result. Add GEMINI_API_KEY for VLM capabilities." | |
| def _build_fallback_chat(message: str, cnn_context: Optional[str], lang: str) -> str: | |
| """ูุฑุฏ ุนูู ุงูุฃุณุฆูุฉ ู ู ูุชูุฌุฉ CNN ุนูุฏู ุง ูููู Gemini ุบูุฑ ู ุชุงุญ""" | |
| ctx = {} | |
| if cnn_context: | |
| try: ctx = json.loads(cnn_context) | |
| except: pass | |
| msg_lower = message.lower() | |
| ar = lang == "ar" | |
| if any(w in msg_lower for w in ["cause","ุณุจุจ","ู ู ุฑุถ","pathogen"]): | |
| return (f"ุณุจุจ ุงูู ุฑุถ: {ctx.get('disease_name_ar') or ctx.get('disease_name_en','ุบูุฑ ู ุญุฏุฏ')}. " | |
| f"ููุน ุงูุนุฏูู: {ctx.get('infection_type','ุบูุฑ ู ุญุฏุฏ')}." if ar else | |
| f"Disease cause: {ctx.get('disease_name_en','Unknown')}. " | |
| f"Infection type: {ctx.get('infection_type','Unknown')}.") | |
| if any(w in msg_lower for w in ["treat","ุนูุงุฌ","cure","ุฏูุงุก","ู ุจูุฏ"]): | |
| return (ctx.get('treatment_ar') or ctx.get('treatment_en') or | |
| ("ุฑุงุฌุน ุจุฑูุชูููู ุงูุนูุงุฌ ูู ูุชูุฌุฉ CNN." if ar else "Refer to the CNN treatment protocol.")) | |
| if any(w in msg_lower for w in ["prevent","ููุงูุฉ","spread","ุงูุชุดุงุฑ"]): | |
| return ("ูู ูุน ุงูุงูุชุดุงุฑ: ุฃุฒู ุงููุจุงุชุงุช ุงูู ุตุงุจุฉุ ุทุจูู ู ุจูุฏุงุช ููุงุฆูุฉุ ูุญุณูู ุชูููุฉ ุงูุญูู." if ar else | |
| "To prevent spread: remove infected plants, apply preventive fungicides, improve field ventilation.") | |
| if any(w in msg_lower for w in ["advice","ูุตูุญุฉ","recommend","ุชูุตูุฉ"]): | |
| return (ctx.get('advice_ar') or ctx.get('advice_en') or | |
| ("ุงุชุจุน ุงูุชูุตูุงุช ุงูู ูุถุญุฉ ูู ูุชูุฌุฉ ุงูุชุดุฎูุต." if ar else | |
| "Follow the recommendations shown in the diagnosis result.")) | |
| return (ctx.get('advice_ar') or ctx.get('advice_en') or | |
| ("ูุฑุฌู ุฅุถุงูุฉ GEMINI_API_KEY ูุชูุนูู ุงูุฏุฑุฏุดุฉ ุงูุฐููุฉ ุงููุงู ูุฉ." if ar else | |
| "Please add GEMINI_API_KEY to enable full AI chat capabilities.")) | |
| def _build_simple_chat_fallback(message: str, initial_report: str, lang: str) -> str: | |
| """ุฑุฏ ุจุฏูู ุจุณูุท ุนูุฏู ุง ูููู Gemini ุบูุฑ ู ุชุงุญ""" | |
| msg_lower = message.lower() | |
| if lang == "ar": | |
| if any(w in msg_lower for w in ["ุณุจุจ", "ูู ุงุฐุง"]): | |
| return "ุณุจุจ ุงูู ุฑุถ ุนุงุฏุฉ ู ุง ูููู ูุทุฑูุงู ุฃู ุจูุชูุฑูุงู ุฃู ููุฑูุณูุงู. ูุฑุฌู ุงูุงุทูุงุน ุนูู ุชูุฑูุฑ ุงูุชุดุฎูุต ุฃุนูุงู." | |
| if any(w in msg_lower for w in ["ุนูุงุฌ", "ู ุจูุฏ"]): | |
| if initial_report: | |
| return f"ุงูุนูุงุฌ ุงูู ูุตู ุจู: {initial_report[:200]}" | |
| return "ุงูุนูุงุฌ ูุนุชู ุฏ ุนูู ููุน ุงูู ุฑุถ. ูุฑุฌู ุงุณุชุดุงุฑุฉ ุฎุจูุฑ ุฒุฑุงุนู." | |
| if any(w in msg_lower for w in ["ู ูุน", "ููุงูุฉ"]): | |
| return "ููููุงูุฉ: ุฃุฒู ุงููุจุงุชุงุช ุงูู ุตุงุจุฉุ ุงุณุชุฎุฏู ู ุจูุฏุงุช ููุงุฆูุฉุ ูุญุณู ุชูููุฉ ุงูุญูู." | |
| return "ุดูุฑุงู ูุณุคุงูู. ููุญุตูู ุนูู ุฅุฌุงุจุฉ ู ูุตูุฉุ ูุฑุฌู ุชุดุบูู ุฎุฏู ุฉ Gemini VLM." | |
| else: | |
| if any(w in msg_lower for w in ["cause", "why"]): | |
| return "The disease is typically caused by fungal, bacterial, or viral pathogens. Check the diagnosis report above." | |
| if any(w in msg_lower for w in ["treat", "cure", "pesticide"]): | |
| if initial_report: | |
| return f"Recommended treatment: {initial_report[:200]}" | |
| return "Treatment depends on the disease type. Please consult an agricultural expert." | |
| if any(w in msg_lower for w in ["prevent", "spread"]): | |
| return "To prevent spread: remove infected plants, apply preventive fungicides, improve field ventilation." | |
| return "Thank you for your question. For a detailed answer, please enable Gemini VLM service." | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # RUN | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) |