skyline-api / main.py
nadjla12's picture
Upload folder using huggingface_hub
03d111c verified
Raw
History Blame Contribute Delete
33.9 kB
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 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 / โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.get("/")
def root():
return {
"status": "Skyline LAND API โœ…",
"cnn_classes": len(CLASS_NAMES),
"vlm_ready": GEMINI_READY,
"version": "3.0.0",
}
# โ”€โ”€ GET /health โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@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,
}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# POST /predict โ† Admin + User (CNN + Grad-CAM)
# ูƒู„ุง ุงู„ุตูุญุชูŠู† ุชุณุชุฏุนูŠ ู‡ุฐุง ุงู„ู€ endpoint
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@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}")
# 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)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@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),
):
# โ”€โ”€ 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 ูู‚ุท (ุฏุฑุฏุดุฉ ู…ุชุนุฏุฏุฉ ุงู„ุฃุฏูˆุงุฑ)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@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),
):
# โ”€โ”€ 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)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@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", "")
# 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 (ุจูŠุงู†ุงุชู‡ ูู‚ุท)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@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}")
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 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)