Aa19999 commited on
Commit
df6b937
·
verified ·
1 Parent(s): 922bbbc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +81 -74
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
- from huggingface_hub import InferenceClient
 
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
- # HuggingFace Inference API for Q&A — no local model loading, no memory issues.
61
- # IMPORTANT: HF moved most VLMs behind paid Inference Providers (Hyperbolic,
62
- # Together, Novita, etc.). An HF_TOKEN is REQUIRED as a Space Secret.
63
- # Models listed in order of preference; we fall through to the next on error.
64
- HF_TOKEN = os.getenv("HF_TOKEN")
65
- VLM_API_MODELS = [
66
- "Qwen/Qwen2.5-VL-7B-Instruct",
67
- "Qwen/Qwen2-VL-7B-Instruct",
68
- "meta-llama/Llama-3.2-11B-Vision-Instruct",
69
- "HuggingFaceM4/idefics3-8b-llama3",
 
 
 
 
 
 
 
 
 
 
 
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 — HuggingFace Inference API (hosted VLMs, no local memory)
 
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
- f"({model_used.split('/')[-1]}). للاستكشاف والتعليم فقط — "
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
- messages = [{
422
- "role": "user",
423
- "content": [
424
- {"type": "image_url", "image_url": {"url": data_url}},
425
- {"type": "text", "text": q},
426
- ],
427
- }]
428
 
429
- # Try multiple models in order — fall back on failure
430
  errors = []
431
- for model_id in VLM_API_MODELS:
432
  try:
433
- response = inference_client.chat_completion(
434
- messages=messages,
435
- model=model_id,
436
- max_tokens=500,
437
- temperature=0.4,
438
  )
439
- answer = response.choices[0].message.content.strip()
 
 
 
 
 
 
 
440
  if answer:
441
- return _qa_success_html(q, answer, model_id)
 
 
442
  except Exception as e:
443
- errors.append(f"<strong>{model_id.split('/')[-1]}:</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>لم يُضَف <code>HF_TOKEN</code> كـ Secret في إعدادات الـ Space</li>"
464
- "<li>تجاوز حصة الاستخدام المجانية لـ Hugging Face API</li>"
465
- "<li>النموذج غير متاح حالياً على Inference Providers</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
- "(Qwen2.5-VL، Llama 3.2 Vision) عبر "
988
- "HuggingFace Inference API. الإجابة تأخذ "
989
- "~5-10 ثواني. الأسئلة بالإنجليزية تعطي نتائج "
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