File size: 6,972 Bytes
8c5e3a4
 
 
 
 
 
4322841
8c5e3a4
4322841
 
 
4e8ce70
8c5e3a4
 
4322841
 
 
3c07b8f
 
 
 
7553f81
3c07b8f
5cad372
7553f81
3c07b8f
4322841
 
 
3c07b8f
 
812891f
d5e6c7a
 
3c07b8f
8c5e3a4
 
 
4322841
 
 
8c5e3a4
4322841
 
8c5e3a4
 
4322841
 
8c5e3a4
 
4322841
3c07b8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4322841
 
3c07b8f
 
 
4322841
 
3c07b8f
 
 
 
4322841
 
8c5e3a4
 
 
 
 
 
4322841
 
8c5e3a4
 
 
4322841
 
8c5e3a4
 
 
 
3c07b8f
 
 
4322841
8c5e3a4
 
7553f81
8c5e3a4
4322841
8c5e3a4
 
 
 
 
4322841
8c5e3a4
4322841
 
3c07b8f
8c5e3a4
 
3c07b8f
 
 
 
8c5e3a4
4322841
 
 
8c5e3a4
 
4322841
3c07b8f
 
4322841
3c07b8f
 
 
 
4322841
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c07b8f
d5e6c7a
4322841
8c5e3a4
 
4322841
 
 
 
 
 
 
 
 
3c07b8f
4322841
3c07b8f
 
4322841
 
3c07b8f
 
7553f81
4322841
 
 
 
 
3c07b8f
4322841
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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()