Upload 5 files
Browse files- src/__init__.py +0 -0
- src/ai_doctor.py +82 -0
- src/analyzer.py +73 -0
- src/lab_extractor.py +100 -0
- src/ocr_reader.py +25 -0
src/__init__.py
ADDED
|
File without changes
|
src/ai_doctor.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
class AIDoctor:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.api_key = os.getenv("API_KEY")
|
| 10 |
+
if not self.api_key:
|
| 11 |
+
raise ValueError("❌ خطا: کلید API پیدا نشد.")
|
| 12 |
+
self.url = "https://apifreellm.com/api/v1/chat"
|
| 13 |
+
|
| 14 |
+
def generate_explanation(self, analysis_report: list, patient_gender: str = "male", patient_age: int = None) -> str:
|
| 15 |
+
report_summary = ""
|
| 16 |
+
critical_points = []
|
| 17 |
+
|
| 18 |
+
for item in analysis_report:
|
| 19 |
+
status_emoji = "🔴 مشکل" if item['status'] != 'NORMAL' else "🟢 نرمال"
|
| 20 |
+
line = f"- {item['name_fa']}: {item['value']} {item['unit']} ({status_emoji})"
|
| 21 |
+
report_summary += line + "\n"
|
| 22 |
+
if item['status'] != 'NORMAL':
|
| 23 |
+
critical_points.append(f"{item['name_fa']} ({item['value']})")
|
| 24 |
+
|
| 25 |
+
if not critical_points:
|
| 26 |
+
general_condition = "نتایج کاملاً طبیعی هستند."
|
| 27 |
+
else:
|
| 28 |
+
general_condition = f"چند نتیجه غیرطبیعی: {', '.join(critical_points)}."
|
| 29 |
+
|
| 30 |
+
context_prompt = ""
|
| 31 |
+
if patient_age and patient_age < 18:
|
| 32 |
+
context_prompt = "\n[مهم: بیمار کودک ({patient_age} ساله) است. رنجهای نرمال کودک با بزرگسال فرق داره.]"
|
| 33 |
+
|
| 34 |
+
system_prompt = f"""
|
| 35 |
+
You are a helpful data analyst assistant. Analyze provided laboratory data.
|
| 36 |
+
Write your response in Dari/Persian language.
|
| 37 |
+
|
| 38 |
+
{context_prompt}
|
| 39 |
+
|
| 40 |
+
Instructions:
|
| 41 |
+
1. Start with a friendly greeting.
|
| 42 |
+
2. Summarize general condition based on data.
|
| 43 |
+
3. Explain any numbers flagged as 'problem'.
|
| 44 |
+
4. Provide general health tips based on data.
|
| 45 |
+
5. Important: You are analyzing text data only. Do not act as a doctor. Do not give strict medical prescriptions. Just interpret numbers clearly.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
user_message = f"""
|
| 49 |
+
Patient Gender: {patient_gender}
|
| 50 |
+
Age: {patient_age if patient_age else 'Unknown'}
|
| 51 |
+
General Status: {general_condition}
|
| 52 |
+
|
| 53 |
+
Data Analysis Results:
|
| 54 |
+
{report_summary}
|
| 55 |
+
|
| 56 |
+
Please provide your interpretation:
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
print("🧠 در حال ارسال به API...")
|
| 61 |
+
headers = {
|
| 62 |
+
"Content-Type": "application/json",
|
| 63 |
+
"Authorization": f"Bearer {self.api_key}"
|
| 64 |
+
}
|
| 65 |
+
data = {
|
| 66 |
+
"message": f"{system_prompt}\n\n{user_message}",
|
| 67 |
+
"model": "apifreellm"
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
response = requests.post(self.url, headers=headers, json=data)
|
| 71 |
+
|
| 72 |
+
if response.status_code == 200:
|
| 73 |
+
result = response.json()
|
| 74 |
+
if result.get("success"):
|
| 75 |
+
return result.get("response", "پاسخی دریافت نشد.")
|
| 76 |
+
else:
|
| 77 |
+
return f"❌ خطای AI: {result.get('response')}"
|
| 78 |
+
else:
|
| 79 |
+
return f"❌ خطای سرور: {response.status_code}"
|
| 80 |
+
|
| 81 |
+
except Exception as e:
|
| 82 |
+
return f"❌ خطای اتصال: {str(e)}"
|
src/analyzer.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from typing import Dict, List, Optional
|
| 3 |
+
|
| 4 |
+
class MedicalKnowledgeBase:
|
| 5 |
+
def __init__(self, db_path: str = "medical_db.json"):
|
| 6 |
+
self.db_path = db_path
|
| 7 |
+
self.data = self._load_db()
|
| 8 |
+
|
| 9 |
+
def _load_db(self) -> Dict:
|
| 10 |
+
try:
|
| 11 |
+
with open(self.db_path, 'r', encoding='utf-8') as f:
|
| 12 |
+
return json.load(f)
|
| 13 |
+
except FileNotFoundError:
|
| 14 |
+
print(f"خطا: فایل {self.db_path} پیدا نشد.")
|
| 15 |
+
return {}
|
| 16 |
+
|
| 17 |
+
def get_test_info(self, test_code: str):
|
| 18 |
+
return self.data.get(test_code.upper())
|
| 19 |
+
|
| 20 |
+
class BloodAnalyzer:
|
| 21 |
+
def __init__(self, knowledge_base: MedicalKnowledgeBase):
|
| 22 |
+
self.kb = knowledge_base
|
| 23 |
+
|
| 24 |
+
def analyze_single_test(self, test_code: str, value: float, gender: str = "male", age: int = None) -> Dict:
|
| 25 |
+
"""تک تست را تحلیل میکند. سن نیز برای تشخیص کودک استفاده میشود."""
|
| 26 |
+
test_info = self.kb.get_test_info(test_code)
|
| 27 |
+
|
| 28 |
+
if not test_info:
|
| 29 |
+
return {
|
| 30 |
+
"name_en": test_code, "name_fa": test_code,
|
| 31 |
+
"value": value, "unit": "Unknown",
|
| 32 |
+
"min_ref": 0, "max_ref": 0,
|
| 33 |
+
"status": "UNKNOWN", "message": "این آزمایش در پایگاه داده ما وجود ندارد."
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
name_fa = test_info["name_fa"]
|
| 37 |
+
unit = test_info["unit"]
|
| 38 |
+
ranges = test_info["ranges"]
|
| 39 |
+
|
| 40 |
+
# انتخاب رنج: کودک > جنسیت > پیشفرض
|
| 41 |
+
if age and age < 18 and "child" in ranges:
|
| 42 |
+
min_val, max_val = ranges["child"]
|
| 43 |
+
elif gender == "male" and "male" in ranges:
|
| 44 |
+
min_val, max_val = ranges["male"]
|
| 45 |
+
elif gender == "female" and "female" in ranges:
|
| 46 |
+
min_val, max_val = ranges["female"]
|
| 47 |
+
else:
|
| 48 |
+
min_val, max_val = ranges["default"]
|
| 49 |
+
|
| 50 |
+
status = "NORMAL"
|
| 51 |
+
message = "نتیجه طبیعی است."
|
| 52 |
+
|
| 53 |
+
if value > max_val:
|
| 54 |
+
status = "HIGH"
|
| 55 |
+
message = test_info["conditions"]["high"]
|
| 56 |
+
elif value < min_val:
|
| 57 |
+
status = "LOW"
|
| 58 |
+
message = test_info["conditions"]["low"]
|
| 59 |
+
|
| 60 |
+
return {
|
| 61 |
+
"name_en": test_code, "name_fa": name_fa,
|
| 62 |
+
"value": value, "unit": unit,
|
| 63 |
+
"min_ref": min_val, "max_ref": max_val,
|
| 64 |
+
"status": status, "message": message
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
def generate_full_report(self, input_data: Dict[str, float], gender: str = "male", age: int = None):
|
| 68 |
+
"""تمام تستها را تحلیل کرده و لیست برمیگرداند."""
|
| 69 |
+
report = []
|
| 70 |
+
for code, value in input_data.items():
|
| 71 |
+
result = self.analyze_single_test(code, value, gender, age)
|
| 72 |
+
report.append(result)
|
| 73 |
+
return report
|
src/lab_extractor.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
class LabDataExtractor:
|
| 4 |
+
def __init__(self):
|
| 5 |
+
self.targets = {
|
| 6 |
+
"AST": {"name_en": ["AST", "GOT"], "name_fa": "آنزیم کبدی AST"},
|
| 7 |
+
"ALAT": {"name_en": ["ALAT", "GPT", "ALT"], "name_fa": "آنزیم کبدی ALAT"},
|
| 8 |
+
"BILIRUBIN": {"name_en": ["Bilirubin", "T.Bil"], "name_fa": "بیلیروبین کل"},
|
| 9 |
+
"WBC": {"name_en": ["WBC"], "name_fa": "گلبول سفید"},
|
| 10 |
+
"RBC": {"name_en": ["RBC"], "name_fa": "گلبول قرمز"},
|
| 11 |
+
"HGB": {
|
| 12 |
+
"name_en": ["HGB", "Hemoglobin", "Hb", "Haemoglobin"],
|
| 13 |
+
"name_fa": "هموگلوبین"
|
| 14 |
+
},
|
| 15 |
+
"HCT": {"name_en": ["HCT", "Hematocrit"], "name_fa": "هماتوکریت"},
|
| 16 |
+
"PLT": {"name_en": ["PLT", "Platelets"], "name_fa": "پلاکت"},
|
| 17 |
+
"MCV": {"name_en": ["MCV"], "name_fa": "حجم گلبول (MCV)"},
|
| 18 |
+
"MCH": {"name_en": ["MCH"], "name_fa": "هموگلوبین گلبول (MCH)"},
|
| 19 |
+
"MCHC": {"name_en": ["MCHC"], "name_fa": "غلظت هموگلوبین (MCHC)"},
|
| 20 |
+
"FBS": {"name_en": ["FBS", "Glucose"], "name_fa": "قند ناشتا"},
|
| 21 |
+
"CHOL": {"name_en": ["Cholesterol", "CHOL"], "name_fa": "کلسترول کل"},
|
| 22 |
+
"HDL": {"name_en": ["HDL"], "name_fa": "کلسترول خوب"},
|
| 23 |
+
"LDL": {"name_en": ["LDL"], "name_fa": "کلسترول بد"},
|
| 24 |
+
"TG": {"name_en": ["Triglycerides", "TG"], "name_fa": "تریگلیسرید"},
|
| 25 |
+
"UREA": {"name_en": ["Urea", "BUN"], "name_fa": "اوره خون"},
|
| 26 |
+
"CREAT": {"name_en": ["Creatinine", "Crea"], "name_fa": "کراتینین"},
|
| 27 |
+
"ALP": {"name_en": ["Alkaline Phosphatase", "ALP"], "name_fa": "فسفاتاز قلیایی"},
|
| 28 |
+
"TSH": {"name_en": ["TSH"], "name_fa": "هورمون تیروئید"},
|
| 29 |
+
"IRON": {"name_en": ["Iron", "Serum Iron"], "name_fa": "آهن"},
|
| 30 |
+
"FERRITIN": {"name_en": ["Ferritin"], "name_fa": "فریتین"}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
def get_patient_age(self, raw_text: str) -> int:
|
| 34 |
+
"""سن بیمار را از متن پیدا میکند."""
|
| 35 |
+
age_match = re.search(r'Age[:\s]*(\d+)', raw_text, re.IGNORECASE)
|
| 36 |
+
if not age_match:
|
| 37 |
+
age_match = re.search(r'(\d+)\s*Years', raw_text, re.IGNORECASE)
|
| 38 |
+
if age_match:
|
| 39 |
+
return int(age_match.group(1))
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
def is_lab_id(self, text, num_obj):
|
| 43 |
+
"""چک میکند که آیا عدد شماره آزمایش (ID/No) است یا نه."""
|
| 44 |
+
start_index = num_obj['start']
|
| 45 |
+
context_start = max(0, start_index - 20)
|
| 46 |
+
context_text = text[context_start:start_index]
|
| 47 |
+
id_keywords = ["no", "id", "ref", "reg", "scl", "lab", "time", "collection", "admission"]
|
| 48 |
+
if any(keyword in context_text.lower() for keyword in id_keywords):
|
| 49 |
+
return True
|
| 50 |
+
return False
|
| 51 |
+
|
| 52 |
+
def find_number_nearby(self, text, keyword):
|
| 53 |
+
"""این متد اسم آزمایش رو پیدا میکنه و نزدیکترین عدد رو در کل متن بهش پیدا میکند."""
|
| 54 |
+
match_pos = -1
|
| 55 |
+
matched_name = None
|
| 56 |
+
for name in keyword:
|
| 57 |
+
# تغییر مهم: استفاده از \b برای دقیق بودن جستجو (رفع باگ ALFALAH)
|
| 58 |
+
pattern = r'\b' + re.escape(name) + r'\b'
|
| 59 |
+
match = re.search(pattern, text, re.IGNORECASE)
|
| 60 |
+
if match:
|
| 61 |
+
match_pos = match.start()
|
| 62 |
+
matched_name = name
|
| 63 |
+
break
|
| 64 |
+
|
| 65 |
+
if match_pos == -1:
|
| 66 |
+
return None
|
| 67 |
+
|
| 68 |
+
number_positions = []
|
| 69 |
+
for match in re.finditer(r'(?<![<>])\b\d+[.,]?\d*\b', text):
|
| 70 |
+
if not self.is_lab_id(text, {'start': match.start()}):
|
| 71 |
+
number_positions.append({
|
| 72 |
+
'value': float(match.group().replace(',', '.')),
|
| 73 |
+
'start': match.start()
|
| 74 |
+
})
|
| 75 |
+
|
| 76 |
+
if not number_positions:
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
closest_num = None
|
| 80 |
+
min_distance = float('inf')
|
| 81 |
+
max_allowed_dist = 300
|
| 82 |
+
|
| 83 |
+
for num_obj in number_positions:
|
| 84 |
+
dist = abs(num_obj['start'] - match_pos)
|
| 85 |
+
if dist < min_distance:
|
| 86 |
+
min_distance = dist
|
| 87 |
+
closest_num = num_obj
|
| 88 |
+
|
| 89 |
+
if closest_num and min_distance < max_allowed_dist:
|
| 90 |
+
return closest_num['value']
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
def extract_all(self, raw_text: str) -> dict:
|
| 94 |
+
clean_raw = " ".join(raw_text.split())
|
| 95 |
+
extracted_data = {}
|
| 96 |
+
for test_code, info in self.targets.items():
|
| 97 |
+
value = self.find_number_nearby(clean_raw, info["name_en"])
|
| 98 |
+
if value:
|
| 99 |
+
extracted_data[test_code] = value
|
| 100 |
+
return extracted_data
|
src/ocr_reader.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import easyocr
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
class OCRReader:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
print("🧠 در حال بارگذاری موتور EasyOCR (این دفعه راحتتره)...")
|
| 7 |
+
# gpu=False یعنی از پردازنده گرافیکی استفاده نکن (برای لپتاپهای معمولی بهتره)
|
| 8 |
+
# ['en'] یعنی متن انگلیسی و اعداد رو بخون
|
| 9 |
+
self.reader = easyocr.Reader(['en'], gpu=False)
|
| 10 |
+
print("✅ موتور OCR آماده است.")
|
| 11 |
+
|
| 12 |
+
def extract_text(self, image_path: str) -> str:
|
| 13 |
+
if not os.path.exists(image_path):
|
| 14 |
+
raise FileNotFoundError(f"Image not found: {image_path}")
|
| 15 |
+
|
| 16 |
+
print(f"📖 در حال خواندن فایل {image_path}...")
|
| 17 |
+
|
| 18 |
+
# خواندن متن از عکس
|
| 19 |
+
# detail=0 یعنی فقط متن رو بده، مختصات پیکسل رو نخواه
|
| 20 |
+
results = self.reader.readtext(image_path, detail=0)
|
| 21 |
+
|
| 22 |
+
# تبدیل لیست نتایج به یک رشته متنی
|
| 23 |
+
full_text = "\n".join(results)
|
| 24 |
+
|
| 25 |
+
return full_text
|