Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -37,10 +37,6 @@ _gc_utils.json_schema_to_python_type = _safe_outer
|
|
| 37 |
|
| 38 |
import os
|
| 39 |
|
| 40 |
-
import base64
|
| 41 |
-
import traceback
|
| 42 |
-
from io import BytesIO
|
| 43 |
-
|
| 44 |
import gradio as gr
|
| 45 |
import matplotlib.pyplot as plt
|
| 46 |
import pandas as pd
|
|
@@ -48,7 +44,8 @@ import torch
|
|
| 48 |
import torch.nn.functional as F
|
| 49 |
from PIL import Image
|
| 50 |
from transformers import CLIPModel, CLIPProcessor
|
| 51 |
-
|
|
|
|
| 52 |
|
| 53 |
# ---------------------------------------------------------------------------
|
| 54 |
# Settings
|
|
@@ -57,18 +54,28 @@ APP_PASSWORD = os.getenv("APP_PASSWORD", "9890")
|
|
| 57 |
PLIP_MODEL_ID = "vinid/plip"
|
| 58 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 59 |
|
| 60 |
-
#
|
| 61 |
-
#
|
| 62 |
-
#
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
]
|
| 71 |
-
inference_client = InferenceClient(token=HF_TOKEN)
|
| 72 |
|
| 73 |
DEFAULT_PROMPT = (
|
| 74 |
"Normal oral mucosa with regular stratified squamous epithelium\n"
|
|
@@ -362,20 +369,9 @@ def diagnose(image, prompt_text):
|
|
| 362 |
|
| 363 |
|
| 364 |
# ---------------------------------------------------------------------------
|
| 365 |
-
# Q&A inference —
|
|
|
|
| 366 |
# ---------------------------------------------------------------------------
|
| 367 |
-
def _image_to_data_url(pil_img):
|
| 368 |
-
"""Encode PIL image as base64 data URL for the chat API."""
|
| 369 |
-
# Cap size to keep API request small and fast
|
| 370 |
-
img = pil_img.copy()
|
| 371 |
-
if max(img.size) > 1024:
|
| 372 |
-
img.thumbnail((1024, 1024))
|
| 373 |
-
buf = BytesIO()
|
| 374 |
-
img.save(buf, format="JPEG", quality=88)
|
| 375 |
-
b64 = base64.b64encode(buf.getvalue()).decode()
|
| 376 |
-
return f"data:image/jpeg;base64,{b64}"
|
| 377 |
-
|
| 378 |
-
|
| 379 |
def _warning_html(title, body):
|
| 380 |
return (
|
| 381 |
"<div class='result-card warning-card'>"
|
|
@@ -393,9 +389,8 @@ def _qa_success_html(question, answer, model_used):
|
|
| 393 |
"<div class='qa-question'><strong>السؤال:</strong> "
|
| 394 |
f"{question}</div>"
|
| 395 |
f"<div class='qa-answer'>{answer}</div>"
|
| 396 |
-
"<div class='result-note'>
|
| 397 |
-
|
| 398 |
-
"ليس بديلاً عن المشرّح المرضي.</div>"
|
| 399 |
"</div>"
|
| 400 |
)
|
| 401 |
|
|
@@ -404,7 +399,7 @@ def answer_question(image, question):
|
|
| 404 |
if image is None:
|
| 405 |
return _warning_html(
|
| 406 |
"لم يتم رفع صورة",
|
| 407 |
-
"ارفع صورة نسيجية أولاً ثم اكتب سؤالك.",
|
| 408 |
)
|
| 409 |
|
| 410 |
q = (question or "").strip()
|
|
@@ -414,67 +409,80 @@ def answer_question(image, question):
|
|
| 414 |
"اكتب سؤالك أو طلبك بالإنجليزية (يعطي نتائج أفضل) أو بالعربية.",
|
| 415 |
)
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
pil_img = image if isinstance(image, Image.Image) else Image.fromarray(image)
|
| 418 |
pil_img = pil_img.convert("RGB")
|
| 419 |
-
data_url = _image_to_data_url(pil_img)
|
| 420 |
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
{"type": "image_url", "image_url": {"url": data_url}},
|
| 425 |
-
{"type": "text", "text": q},
|
| 426 |
-
],
|
| 427 |
-
}]
|
| 428 |
|
| 429 |
-
# Try
|
| 430 |
errors = []
|
| 431 |
-
for
|
| 432 |
try:
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
max_tokens=500,
|
| 437 |
-
temperature=0.4,
|
| 438 |
)
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
if answer:
|
| 441 |
-
return _qa_success_html(q, answer,
|
|
|
|
|
|
|
| 442 |
except Exception as e:
|
| 443 |
-
errors.append(f"<strong>{
|
| 444 |
f"{type(e).__name__}: {str(e)[:200]}")
|
| 445 |
continue
|
| 446 |
|
| 447 |
-
# All models failed — show diagnostic info
|
| 448 |
err_list = "<br>• ".join(errors) if errors else "Unknown error"
|
| 449 |
-
token_status = (
|
| 450 |
-
"<span style='color:#27ae60;'>✓ HF_TOKEN موجود</span>"
|
| 451 |
-
if HF_TOKEN
|
| 452 |
-
else "<span style='color:#c0392b;'>✗ HF_TOKEN غير موجود — هذا هو السبب الأرجح</span>"
|
| 453 |
-
)
|
| 454 |
return (
|
| 455 |
"<div class='result-card warning-card'>"
|
| 456 |
"<div class='result-icon'>❌</div>"
|
| 457 |
"<div class='result-title' style='color:#c0392b;'>"
|
| 458 |
"تعذّر الحصول على إجابة</div>"
|
| 459 |
-
"<div class='result-sub' style='margin-bottom:12px;'>"
|
| 460 |
-
f"<strong>حالة المصادقة:</strong> {token_status}</div>"
|
| 461 |
"<div class='result-sub'>الأسباب المحتملة:"
|
| 462 |
"<ul style='text-align:right;margin-top:10px;'>"
|
| 463 |
-
"<li>لم
|
| 464 |
-
"<li>تجاوز حصة الا
|
| 465 |
-
"<li>ال
|
| 466 |
-
"<li>مشكلة شبكة مؤقتة</li>"
|
| 467 |
"</ul></div>"
|
| 468 |
"<details style='text-align:left;direction:ltr;margin-top:12px;'>"
|
| 469 |
"<summary style='cursor:pointer;color:#7f8c8d;'>تفاصيل تقنية</summary>"
|
| 470 |
f"<pre style='font-size:0.8em;white-space:pre-wrap;'>• {err_list}</pre>"
|
| 471 |
"</details>"
|
| 472 |
-
"<div class='result-note' style='margin-top:14px;'>"
|
| 473 |
-
"💡 الحل: أضف <code>HF_TOKEN</code> من "
|
| 474 |
-
"<a href='https://huggingface.co/settings/tokens' target='_blank'>"
|
| 475 |
-
"huggingface.co/settings/tokens</a> ثم Settings → "
|
| 476 |
-
"Variables and secrets في الـ Space."
|
| 477 |
-
"</div>"
|
| 478 |
"</div>"
|
| 479 |
)
|
| 480 |
|
|
@@ -983,11 +991,10 @@ with gr.Blocks(title="تشخيص OED — PLIP",
|
|
| 983 |
)
|
| 984 |
gr.HTML(
|
| 985 |
"<div class='prompt-hint'>"
|
| 986 |
-
"💡 يستخدم هذا الوضع نم
|
| 987 |
-
"
|
| 988 |
-
"
|
| 989 |
-
"
|
| 990 |
-
"أدق."
|
| 991 |
"</div>"
|
| 992 |
)
|
| 993 |
|
|
|
|
| 37 |
|
| 38 |
import os
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
import gradio as gr
|
| 41 |
import matplotlib.pyplot as plt
|
| 42 |
import pandas as pd
|
|
|
|
| 44 |
import torch.nn.functional as F
|
| 45 |
from PIL import Image
|
| 46 |
from transformers import CLIPModel, CLIPProcessor
|
| 47 |
+
|
| 48 |
+
import google.generativeai as genai
|
| 49 |
|
| 50 |
# ---------------------------------------------------------------------------
|
| 51 |
# Settings
|
|
|
|
| 54 |
PLIP_MODEL_ID = "vinid/plip"
|
| 55 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 56 |
|
| 57 |
+
# Google Gemini API — free tier: 1500 requests/day for 2.0-flash, no card needed.
|
| 58 |
+
# Get a free key at: https://aistudio.google.com/app/apikey
|
| 59 |
+
# Add it as a Space Secret named GEMINI_API_KEY.
|
| 60 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 61 |
+
if GEMINI_API_KEY:
|
| 62 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 63 |
+
|
| 64 |
+
# Models tried in order. Newer/stronger first.
|
| 65 |
+
GEMINI_MODELS = [
|
| 66 |
+
"gemini-2.0-flash",
|
| 67 |
+
"gemini-2.0-flash-exp",
|
| 68 |
+
"gemini-1.5-flash",
|
| 69 |
+
"gemini-1.5-flash-8b",
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
# Relaxed safety thresholds — medical/pathology images can be flagged otherwise.
|
| 73 |
+
GEMINI_SAFETY = [
|
| 74 |
+
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
| 75 |
+
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
| 76 |
+
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
|
| 77 |
+
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
|
| 78 |
]
|
|
|
|
| 79 |
|
| 80 |
DEFAULT_PROMPT = (
|
| 81 |
"Normal oral mucosa with regular stratified squamous epithelium\n"
|
|
|
|
| 369 |
|
| 370 |
|
| 371 |
# ---------------------------------------------------------------------------
|
| 372 |
+
# Q&A inference — Google Gemini API
|
| 373 |
+
# Free tier: 1,500 requests/day for gemini-2.0-flash, no credit card required.
|
| 374 |
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
def _warning_html(title, body):
|
| 376 |
return (
|
| 377 |
"<div class='result-card warning-card'>"
|
|
|
|
| 389 |
"<div class='qa-question'><strong>السؤال:</strong> "
|
| 390 |
f"{question}</div>"
|
| 391 |
f"<div class='qa-answer'>{answer}</div>"
|
| 392 |
+
f"<div class='result-note'>🤖 Google Gemini ({model_used}). "
|
| 393 |
+
"للاستكشاف والتعليم فقط — ليس بديلاً عن المشرّح المرضي.</div>"
|
|
|
|
| 394 |
"</div>"
|
| 395 |
)
|
| 396 |
|
|
|
|
| 399 |
if image is None:
|
| 400 |
return _warning_html(
|
| 401 |
"لم يتم رفع صورة",
|
| 402 |
+
"ارفع صورة نسيجية أولاً (في خانة الصورة بالأعلى) ثم اكتب سؤالك.",
|
| 403 |
)
|
| 404 |
|
| 405 |
q = (question or "").strip()
|
|
|
|
| 409 |
"اكتب سؤالك أو طلبك بالإنجليزية (يعطي نتائج أفضل) أو بالعربية.",
|
| 410 |
)
|
| 411 |
|
| 412 |
+
if not GEMINI_API_KEY:
|
| 413 |
+
return (
|
| 414 |
+
"<div class='result-card warning-card'>"
|
| 415 |
+
"<div class='result-icon'>🔑</div>"
|
| 416 |
+
"<div class='result-title' style='color:#c0392b;'>"
|
| 417 |
+
"GEMINI_API_KEY غير مُعرَّف</div>"
|
| 418 |
+
"<div class='result-sub'>"
|
| 419 |
+
"يحتاج هذا الوضع مفتاح Google Gemini API (مجاني تماماً). "
|
| 420 |
+
"احصل عليه من:"
|
| 421 |
+
"<br><br>"
|
| 422 |
+
"🔗 <a href='https://aistudio.google.com/app/apikey' target='_blank'>"
|
| 423 |
+
"https://aistudio.google.com/app/apikey</a>"
|
| 424 |
+
"<br><br>"
|
| 425 |
+
"ثم أضفه في Space → <strong>Settings</strong> → "
|
| 426 |
+
"<strong>Variables and secrets</strong> → "
|
| 427 |
+
"<strong>New secret</strong>:"
|
| 428 |
+
"<ul style='text-align:right;margin-top:8px;'>"
|
| 429 |
+
"<li><strong>Name:</strong> <code>GEMINI_API_KEY</code></li>"
|
| 430 |
+
"<li><strong>Value:</strong> الصق المفتاح</li>"
|
| 431 |
+
"</ul>"
|
| 432 |
+
"ثم اضغط <strong>Restart Space</strong>."
|
| 433 |
+
"</div>"
|
| 434 |
+
"</div>"
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
pil_img = image if isinstance(image, Image.Image) else Image.fromarray(image)
|
| 438 |
pil_img = pil_img.convert("RGB")
|
|
|
|
| 439 |
|
| 440 |
+
# Resize large images to keep API requests fast
|
| 441 |
+
if max(pil_img.size) > 1280:
|
| 442 |
+
pil_img.thumbnail((1280, 1280))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
|
| 444 |
+
# Try models in order, fall through on failure
|
| 445 |
errors = []
|
| 446 |
+
for model_name in GEMINI_MODELS:
|
| 447 |
try:
|
| 448 |
+
gemini = genai.GenerativeModel(
|
| 449 |
+
model_name,
|
| 450 |
+
safety_settings=GEMINI_SAFETY,
|
|
|
|
|
|
|
| 451 |
)
|
| 452 |
+
response = gemini.generate_content(
|
| 453 |
+
[q, pil_img],
|
| 454 |
+
generation_config={
|
| 455 |
+
"temperature": 0.4,
|
| 456 |
+
"max_output_tokens": 800,
|
| 457 |
+
},
|
| 458 |
+
)
|
| 459 |
+
answer = (response.text or "").strip()
|
| 460 |
if answer:
|
| 461 |
+
return _qa_success_html(q, answer, model_name)
|
| 462 |
+
errors.append(f"<strong>{model_name}:</strong> empty response "
|
| 463 |
+
"(possibly blocked by safety filter)")
|
| 464 |
except Exception as e:
|
| 465 |
+
errors.append(f"<strong>{model_name}:</strong> "
|
| 466 |
f"{type(e).__name__}: {str(e)[:200]}")
|
| 467 |
continue
|
| 468 |
|
|
|
|
| 469 |
err_list = "<br>• ".join(errors) if errors else "Unknown error"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
return (
|
| 471 |
"<div class='result-card warning-card'>"
|
| 472 |
"<div class='result-icon'>❌</div>"
|
| 473 |
"<div class='result-title' style='color:#c0392b;'>"
|
| 474 |
"تعذّر الحصول على إجابة</div>"
|
|
|
|
|
|
|
| 475 |
"<div class='result-sub'>الأسباب المحتملة:"
|
| 476 |
"<ul style='text-align:right;margin-top:10px;'>"
|
| 477 |
+
"<li>المفتاح <code>GEMINI_API_KEY</code> غير صالح أو منتهي</li>"
|
| 478 |
+
"<li>تجاوز حصة الـ 1,500 طلب اليومية المجانية</li>"
|
| 479 |
+
"<li>الصورة كبيرة جداً (سيُعاد ضغطها تلقائياً)</li>"
|
| 480 |
+
"<li>مشكلة شبكة مؤقتة (جرّب بعد دقيقة)</li>"
|
| 481 |
"</ul></div>"
|
| 482 |
"<details style='text-align:left;direction:ltr;margin-top:12px;'>"
|
| 483 |
"<summary style='cursor:pointer;color:#7f8c8d;'>تفاصيل تقنية</summary>"
|
| 484 |
f"<pre style='font-size:0.8em;white-space:pre-wrap;'>• {err_list}</pre>"
|
| 485 |
"</details>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
"</div>"
|
| 487 |
)
|
| 488 |
|
|
|
|
| 991 |
)
|
| 992 |
gr.HTML(
|
| 993 |
"<div class='prompt-hint'>"
|
| 994 |
+
"💡 يستخدم هذا الوضع نموذج <strong>"
|
| 995 |
+
"Google Gemini 2.0 Flash</strong> (مجاني، "
|
| 996 |
+
"1500 طلب/يوم). الإجابة تأخذ ~3-5 ثواني. "
|
| 997 |
+
"الأسئلة بالإنجليزية تعطي نتائج أدق."
|
|
|
|
| 998 |
"</div>"
|
| 999 |
)
|
| 1000 |
|