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