skyline-api / main.py
nadjla12's picture
fix model download path + add google-generativeai
dd33677
Raw
History Blame Contribute Delete
27.8 kB
# ════════════════════════════════════════════════════════════════
# SKYLINE LAND — main.py (CNN + VLM — نسخة موحدة آمنة)
# ════════════════════════════════════════════════════════════════
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()
# ════════════════════════════════════════════════════════════════
# DOWNLOAD MODEL FROM HUGGING FACE
# ════════════════════════════════════════════════════════════════
from huggingface_hub import hf_hub_download, snapshot_download
HF_REPO = "nadjla12/skyline-api"
def download_models_if_needed():
if not os.path.exists("./models/skyline_savedmodel/saved_model.pb"):
print("⏳ Downloading model from Hugging Face...")
os.makedirs("./models/skyline_savedmodel/variables", exist_ok=True)
# تحميل الملفات واحداً واحداً في المسار الصحيح
files_to_download = [
("models/skyline_savedmodel/saved_model.pb", "./models/skyline_savedmodel/saved_model.pb"),
("models/skyline_savedmodel/variables/variables.index", "./models/skyline_savedmodel/variables/variables.index"),
("models/skyline_savedmodel/variables/variables.data-00000-of-00001", "./models/skyline_savedmodel/variables/variables.data-00000-of-00001"),
]
for repo_path, local_path in files_to_download:
try:
downloaded = hf_hub_download(
repo_id=HF_REPO,
filename=repo_path,
local_dir="./",
)
print(f"✅ Downloaded: {repo_path}")
except Exception as e:
print(f"⚠️ Failed to download {repo_path}: {e}")
# تحميل expert_db.json
try:
hf_hub_download(
repo_id=HF_REPO,
filename="models/expert_db.json",
local_dir="./",
)
print("✅ Downloaded: expert_db.json")
except Exception as e:
print(f"⚠️ Failed to download expert_db.json: {e}")
print("✅ Model download complete!")
else:
print("✅ Model already exists locally.")
download_models_if_needed()
# ════════════════════════════════════════════════════════════════
# 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
# ════════════════════════════════════════════════════════════════
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])
orig_buf = io.BytesIO()
Image.fromarray(img_array.astype(np.uint8)).save(orig_buf, format='PNG')
original_b64 = base64.b64encode(orig_buf.getvalue()).decode()
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 = 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
# ════════════════════════════════════════════════════════════════
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=["*"],
)
@app.get("/")
def root():
return {
"status": "Skyline LAND API ✅",
"cnn_classes": len(CLASS_NAMES),
"vlm_ready": GEMINI_READY,
"version": "3.0.0",
}
@app.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,
}
@app.post("/predict")
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}")
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
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
]
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")
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,
})
@app.post("/vlm-analyze")
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),
):
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"})
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."""
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)
return JSONResponse({"success": True, "analysis": response.text, "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"})
@app.post("/vlm-chat")
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),
):
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"})
sys_prompt = "أنت خبير زراعي متخصص. أجب باللغة العربية فقط وبأسلوب بسيط مناسب للمزارع." if lang == "ar" else \
"You are an expert agricultural advisor. Reply only in English, clearly and practically for a farmer."
if cnn_context:
try:
ctx = json.loads(cnn_context)
sys_prompt += f" CNN detected: '{ctx.get('disease_name_en')}' ({ctx.get('confidence', 0):.1f}% confidence)."
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:]:
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"})
@app.post("/chat")
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", "")
if not GEMINI_READY or gemini_model is None:
fb = _build_simple_chat_fallback(message, initial_report, lang)
return JSONResponse({"success": True, "response": fb, "reply": fb, "source": "fallback"})
context = f"Previous diagnosis report: {initial_report[:500]}\n\n" if initial_report else ""
system_prompt = f"أنت خبير زراعي متخصص في أمراض النبات.\n{context}أجب باللغة العربية فقط، بأسلوب بسيط وعملي." if lang == "ar" else \
f"You are an expert agricultural plant pathologist.\n{context}Answer in English only, clearly and practically."
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}")
fb = _build_simple_chat_fallback(message, initial_report, lang)
return JSONResponse({"success": True, "response": fb, "reply": fb, "source": "error_fallback"})
except Exception as e:
print(f"⚠️ Chat endpoint error: {e}")
return JSONResponse({"success": False, "response": "Chat service temporarily unavailable.", "reply": "Chat service temporarily unavailable.", "source": "error"})
@app.get("/history")
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}")
def _build_fallback_analysis(cnn_context: Optional[str], lang: str) -> str:
if not cnn_context:
return "لا يمكن الاتصال بـ Gemini VLM حالياً." if lang == "ar" else "Gemini VLM is not available."
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', 'استشر خبيراً زراعياً.')}")
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.')}")
except:
return "Analysis based on CNN result."
def _build_fallback_chat(message: str, cnn_context: Optional[str], lang: str) -> str:
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','غير محدد')}. نوع العدوى: {ctx.get('infection_type','غير محدد')}." if ar else
f"Disease cause: {ctx.get('disease_name_en','Unknown')}. 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 ("راجع بروتوكول العلاج." 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."
return ctx.get('advice_ar') or ctx.get('advice_en') or ("اتبع التوصيات الموضحة في نتيجة التشخيص." if ar else "Follow the recommendations shown in the diagnosis result.")
def _build_simple_chat_fallback(message: str, initial_report: str, lang: str) -> str:
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 ["علاج", "مبيد"]):
return f"العلاج الموصى به: {initial_report[:200]}" if initial_report else "العلاج يعتمد على نوع المرض."
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."
if any(w in msg_lower for w in ["treat", "cure", "pesticide"]):
return f"Recommended treatment: {initial_report[:200]}" if initial_report else "Treatment depends on the disease type."
if any(w in msg_lower for w in ["prevent", "spread"]):
return "To prevent spread: remove infected plants, apply preventive fungicides."
return "Thank you for your question. Please enable Gemini VLM service for detailed answers."
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)