sohug's picture
Add reportable
5cad372 verified
import gradio as gr
import requests
import base64
import io
import time
import os
from PIL import Image
# ==========================
# CONFIGURATION
# ==========================
MODEL_ID = "gemini-2.5-pro"
API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL_ID}:generateContent"
MAX_RETRIES = 3
GEMINI_ENV_KEY = "GEMINI_API_KEY"
SYSTEM_INSTRUCTION = (
"""
You are an AI dermatology assistant designed to help patients understand possible skin conditions from images.
Your goal is to offer an informative, empathetic explanation in clear, simple language.
Avoid technical jargon.
Keep your answer brief and concise.
No need to give any disclaimer or introduction.
Generate answers in reportable Bengali language.
"""
)
USER_QUERY = (
"""
Analyze this image of a skin lesion and provide:
1. A brief description of what you observed in the image that supports your conclusion.
2. The diagnosis as Disease Name.
3. A brief explanation of the condition for the patient — including typical symptoms, possible causes, and self-care advice.
"""
)
# ==========================
# HELPER FUNCTIONS
# ==========================
def pil_to_base64(image: Image.Image) -> str:
"""Convert PIL Image to base64-encoded JPEG."""
buffer = io.BytesIO()
if image.mode != "RGB":
image = image.convert("RGB")
image.save(buffer, format="JPEG")
return base64.b64encode(buffer.getvalue()).decode("utf-8")
def generate_gemini_analysis(image: Image.Image, max_output_tokens=2048, temperature=0.0):
"""Send image to Gemini API and return dermatological analysis with a live spinner and status updates."""
def build_spinner_html(status: str = "Working...", hidden: bool = False) -> str:
display = "none" if hidden else "flex"
return f"""
<div style="display:{display};align-items:center;gap:12px;justify-content:center;">
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;">
<div style="
width:48px;height:48px;
border:6px solid rgba(255,255,255,0.12);
border-top-color:#06b6d4;
border-radius:50%;
animation: spin 1s linear infinite;
"></div>
<div style="color:#ddd;font-size:0.95em;min-width:180px;text-align:center;">{status}</div>
</div>
</div>
<style>
@keyframes spin {{ from {{ transform: rotate(0deg); }} to {{ transform: rotate(360deg); }} }}
</style>
"""
# initial spinner
yield build_spinner_html("Initializing..."), ""
api_key = os.environ.get(GEMINI_ENV_KEY)
if not api_key:
# hide spinner when showing error/result
yield build_spinner_html("Missing API key", hidden=True), "⚠️ **Missing API key.** Set GEMINI_API_KEY as an environment variable."
return
if not image:
yield build_spinner_html("No image provided", hidden=True), "⚠️ Please upload an image."
return
yield build_spinner_html("Preparing image..."), ""
image_b64 = pil_to_base64(image)
payload = {
"contents": [
{
"role": "user",
"parts": [
{"text": USER_QUERY},
{"inlineData": {"mimeType": "image/jpeg", "data": image_b64}},
],
}
],
"systemInstruction": {"parts": [{"text": SYSTEM_INSTRUCTION}]},
"generationConfig": {"temperature": temperature, "maxOutputTokens": max_output_tokens},
}
headers = {"Content-Type": "application/json"}
# before sending
yield build_spinner_html("Sending to Gemini..."), ""
# Retry logic
for attempt in range(MAX_RETRIES):
try:
yield build_spinner_html("Working..."), ""
response = requests.post(
f"{API_URL}?key={api_key}",
headers=headers,
json=payload,
timeout=90,
)
response.raise_for_status()
data = response.json()
break
except requests.exceptions.RequestException as e:
if attempt < MAX_RETRIES - 1:
yield build_spinner_html("Network error — retrying..."), ""
time.sleep(2 ** attempt)
continue
yield build_spinner_html("Request failed", hidden=True), f"❌ Request failed: {e}"
return
yield build_spinner_html("Parsing response..."), ""
candidate = data.get("candidates", [{}])[0]
parts = candidate.get("content", {}).get("parts", [])
generated_text = "".join(part.get("text", "") for part in parts)
if not generated_text:
finish_reason = candidate.get("finishReason", "")
yield build_spinner_html("No output", hidden=True), f"⚠️ No text output from model. Finish reason: `{finish_reason}`"
return
final_md = f"### 🩺 Dermatological Result \n\n{generated_text.strip()}"
# hide spinner, show final result
yield build_spinner_html("Completed", hidden=True), final_md
# ==========================
# CUSTOM STYLING
# ==========================
css = """
#container {
margin: 0 auto;
max-width: 600px;
text-align: center;
}
#analyze-button {
display: flex;
justify-content: center;
margin-top: 15px;
}
#analyze-button button {
width: auto !important;
padding: 6px 18px !important;
border-radius: 10px !important;
font-weight: 600;
}
.sample-img {
margin-top: 25px;
text-align: center;
opacity: 0.9;
}
.sample-img img {
border-radius: 10px;
width: 220px;
border: 1px solid #444;
}
"""
# ==========================
# GRADIO UI
# ==========================
with gr.Blocks(css=css, title="Skin Disease Analyzer") as demo:
with gr.Column(elem_id="container"):
gr.Markdown(
"""
# Skin Disease Test
**এআই ডাক্তারকে আক্রান্ত স্কিনের একটি ছবি দিন**
"""
)
image_input = gr.Image(
label="Upload Image",
type="pil",
show_label=False,
height=350,
)
# Centered analyze button (smaller)
with gr.Row(elem_id="analyze-button"):
analyze_btn = gr.Button("Test", variant="primary")
# Progress HTML + Result Markdown
progress_html = gr.HTML()
output_md = gr.Markdown(label="Result")
# Working sample image (local file)
gr.Markdown('<p style="font-size:0.9em;margin-top:50px;">📸 Sample Skin Lesion</p>')
gr.Image(value="assets/bdsd.jpg", show_label=False, interactive=False, height=300)
# Event binding
analyze_btn.click(
fn=generate_gemini_analysis,
inputs=[image_input],
outputs=[progress_html, output_md],
)
demo.launch()