| import os |
| import io |
| import threading |
| import torch |
|
|
| from PIL import Image |
| from fastapi import FastAPI, UploadFile, File, Form |
| from fastapi.responses import JSONResponse |
|
|
| from unsloth import FastVisionModel |
| from transformers import TextIteratorStreamer |
| from groq import Groq |
|
|
| MODEL_REPO = "Rady10/Plant-Disease-Qwen3VL-2B" |
|
|
| MAX_SEQ_LENGTH = 512 |
|
|
| VISION_MAX_NEW_TOKENS = 256 |
|
|
| SUMMARY_MAX_TOKENS = 220 |
|
|
| GROQ_API_KEY = os.environ["GROQ_API_KEY"] |
|
|
| GROQ_MODEL = "llama-3.3-70b-versatile" |
|
|
| print("Loading vision model...") |
|
|
| model, tokenizer = FastVisionModel.from_pretrained( |
| MODEL_REPO, |
| load_in_4bit=True, |
| max_seq_length=MAX_SEQ_LENGTH, |
| use_gradient_checkpointing="unsloth", |
| ) |
|
|
| FastVisionModel.for_inference(model) |
|
|
| print("Vision model loaded.") |
|
|
| groq_client = Groq(api_key=GROQ_API_KEY) |
|
|
| VISION_SYSTEM_PROMPT = """ |
| You are a professional plant disease expert. |
| |
| You identify diseases from plant images and answer agriculture questions. |
| """ |
|
|
| SUMMARY_SYSTEM_PROMPT = """ |
| You are an agriculture assistant. |
| |
| Rewrite the diagnosis in simple Egyptian Arabic. |
| |
| Rules: |
| - 3-6 short sentences. |
| - Mention disease. |
| - Mention likely cause. |
| - Mention treatment. |
| - Do not invent information. |
| """ |
|
|
| def run_vision_model(image, question): |
|
|
| messages = [ |
| { |
| "role": "system", |
| "content": [ |
| { |
| "type":"text", |
| "text":VISION_SYSTEM_PROMPT |
| } |
| ] |
| }, |
| { |
| "role":"user", |
| "content":[ |
| { |
| "type":"image", |
| "image":image |
| }, |
| { |
| "type":"text", |
| "text":question or "Diagnose this plant." |
| } |
| ] |
| } |
| ] |
|
|
| prompt = tokenizer.apply_chat_template( |
| messages, |
| add_generation_prompt=True |
| ) |
|
|
| inputs = tokenizer( |
| image, |
| prompt, |
| add_special_tokens=False, |
| return_tensors="pt" |
| ) |
|
|
| if torch.cuda.is_available(): |
| inputs = inputs.to("cuda") |
|
|
| streamer = TextIteratorStreamer( |
| tokenizer, |
| skip_prompt=True, |
| skip_special_tokens=True, |
| ) |
|
|
| kwargs = dict( |
| **inputs, |
| streamer=streamer, |
| max_new_tokens=VISION_MAX_NEW_TOKENS, |
| temperature=0.7, |
| top_p=0.9, |
| repetition_penalty=1.15, |
| ) |
|
|
| thread = threading.Thread( |
| target=model.generate, |
| kwargs=kwargs, |
| ) |
|
|
| thread.start() |
|
|
| text = "" |
|
|
| for piece in streamer: |
| text += piece |
|
|
| return text.strip() |
|
|
| def summarize_with_groq(raw, question): |
|
|
| response = groq_client.chat.completions.create( |
| model=GROQ_MODEL, |
| messages=[ |
| { |
| "role":"system", |
| "content":SUMMARY_SYSTEM_PROMPT |
| }, |
| { |
| "role":"user", |
| "content":f""" |
| Question: |
| {question} |
| |
| Diagnosis: |
| |
| {raw} |
| """ |
| } |
| ], |
| max_tokens=SUMMARY_MAX_TOKENS, |
| temperature=0.6, |
| ) |
|
|
| return response.choices[0].message.content |
|
|
| app = FastAPI(title="Plant Disease API") |
|
|
| @app.post("/analyze") |
| async def analyze( |
| image: UploadFile = File(...), |
| question: str = Form("") |
| ): |
|
|
| image_bytes = await image.read() |
|
|
| image = Image.open(io.BytesIO(image_bytes)).convert("RGB") |
|
|
| raw = run_vision_model(image, question) |
|
|
| summary = summarize_with_groq(raw, question) |
|
|
| return JSONResponse( |
| { |
| "question": question, |
| "summary": summary, |
| "technical_diagnosis": raw |
| } |
| ) |