| import json |
| import os |
| import re |
| import time |
| import urllib.parse |
| from typing import Any, Dict, List, Optional |
|
|
| import gradio as gr |
| import requests |
| import spaces |
| import torch |
| from PIL import Image, ImageDraw, ImageFont |
| from transformers import ( |
| Qwen2VLForConditionalGeneration, |
| AutoProcessor, |
| ) |
|
|
| |
| |
| VL_MODEL_ID = "Qwen/Qwen2-VL-7B-Instruct" |
|
|
|
|
| def search_drug_web_simple(drug_name: str) -> str: |
| """κ°λ¨ν μΉ κ²μμΌλ‘ μ½λ¬Ό μ 보 κ²μ¦""" |
| try: |
| clean_name = re.sub(r'\(.*?\)|\d+mg|\d+mL|μ |ν¬|μΊ‘μ', '', drug_name).strip() |
| sources = [ |
| f"https://www.health.kr/searchIdentity/search_result_detail.asp?searchStr={urllib.parse.quote(clean_name)}", |
| f"https://terms.naver.com/search.naver?query={urllib.parse.quote(clean_name + ' μ½')}" |
| ] |
|
|
| for url in sources: |
| try: |
| response = requests.get(url, timeout=3, headers={'User-Agent': 'Mozilla/5.0'}) |
| if response.status_code == 200 and len(response.text) > 1000: |
| text = response.text[:3000] |
| if any(kw in text for kw in ["ν¨λ₯", "ν¨κ³Ό", "볡μ©", "μ£Όμ"]): |
| return f"β μΉμμ {clean_name} μ 보λ₯Ό μ°Ύμμ΅λλ€." |
| except: |
| continue |
| return "" |
| except: |
| return "" |
|
|
|
|
| def _load_font(): |
| """νκΈ ν°νΈ λ‘λ""" |
| font_path = "NotoSansKR-Regular.ttf" |
| if not os.path.exists(font_path): |
| try: |
| url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/Korean/NotoSansKR-Regular.otf" |
| response = requests.get(url) |
| with open(font_path, "wb") as f: |
| f.write(response.content) |
| except: |
| return None |
| try: |
| return ImageFont.truetype(font_path, 16) |
| except: |
| return None |
|
|
|
|
| DEFAULT_FONT = _load_font() |
|
|
|
|
| def _load_vl_model(): |
| """λμ©λ VL λͺ¨λΈ λ‘λ - μ΅λ νμ§ + ZeroGPU μ΅μ ν""" |
| device_map = "auto" if torch.cuda.is_available() else None |
|
|
| |
| model = Qwen2VLForConditionalGeneration.from_pretrained( |
| VL_MODEL_ID, |
| device_map=device_map, |
| load_in_8bit=True, |
| torch_dtype=torch.float16, |
| trust_remote_code=True, |
| ) |
|
|
| processor = AutoProcessor.from_pretrained(VL_MODEL_ID, trust_remote_code=True) |
| return model, processor |
|
|
|
|
| print("π Loading Qwen2-VL-7B model with 8-bit quantization + quality optimizations...") |
| VL_MODEL, VL_PROCESSOR = _load_vl_model() |
| print("β
Model loaded successfully! (7B @ 8-bit with ultra-quality inference settings)") |
|
|
|
|
| def _extract_assistant_content(decoded: str) -> str: |
| """μ΄μμ€ν΄νΈ μλ΅ μΆμΆ""" |
| if "<|im_start|>assistant" in decoded: |
| content = decoded.split("<|im_start|>assistant")[-1] |
| content = content.replace("<|im_end|>", "").strip() |
| return content |
| return decoded.strip() |
|
|
|
|
| def _extract_json_block(text: str) -> Optional[str]: |
| """JSON λΈλ‘ μΆμΆ""" |
| match = re.search(r"\{.*\}", text, re.DOTALL) |
| if not match: |
| return None |
| return match.group(0) |
|
|
|
|
| def _sanitize_list(value: Any) -> List[str]: |
| """리μ€νΈ μ μ """ |
| if isinstance(value, (list, tuple)): |
| return [str(v).strip() for v in value if str(v).strip()] |
| if isinstance(value, str): |
| return [v.strip() for v in re.split(r"[,;]", value) if v.strip()] |
| return [] |
|
|
|
|
| def _sanitize_medication(item: Dict[str, Any]) -> Dict[str, Any]: |
| """μ½λ¬Ό μ 보 μ μ """ |
| def _to_str(val: Any) -> str: |
| return "" if val is None else str(val).strip() |
|
|
| times = item.get("times_per_day") |
| if isinstance(times, (int, float)): |
| times_str = str(int(times)) if float(times).is_integer() else str(times) |
| else: |
| times_str = _to_str(times) |
|
|
| return { |
| "name": _to_str(item.get("name")), |
| "dose_per_intake": _to_str(item.get("dose_per_intake")), |
| "times_per_day": times_str, |
| "time_slots": _sanitize_list(item.get("time_slots")), |
| "description": _to_str(item.get("description")), |
| "efficacy": _to_str(item.get("efficacy")), |
| "usage_precautions": _to_str(item.get("usage_precautions")), |
| "side_effects": _to_str(item.get("side_effects")), |
| "drug_interactions": _to_str(item.get("drug_interactions")), |
| "warnings": _to_str(item.get("warnings")), |
| } |
|
|
|
|
| def _parse_vl_response(text: str) -> Dict[str, Any]: |
| """VL λͺ¨λΈ μλ΅ νμ±""" |
| json_block = _extract_json_block(text) |
| if not json_block: |
| return { |
| "raw_text": "", |
| "medications": [], |
| "warnings": ["λͺ¨λΈ μλ΅μμ JSON νμμ μ°Ύμ§ λͺ»νμ΅λλ€."], |
| } |
|
|
| try: |
| data = json.loads(json_block) |
| except json.JSONDecodeError: |
| return { |
| "raw_text": "", |
| "medications": [], |
| "warnings": ["JSON νμ± μ€ν¨"], |
| } |
|
|
| meds_raw = data.get("medications") or [] |
| medications = [] |
| if isinstance(meds_raw, list): |
| for item in meds_raw: |
| if isinstance(item, dict): |
| medications.append(_sanitize_medication(item)) |
|
|
| warnings_raw = data.get("warnings") |
| if isinstance(warnings_raw, list): |
| warnings = [str(w).strip() for w in warnings_raw if str(w).strip()] |
| elif warnings_raw: |
| warnings = [str(warnings_raw).strip()] |
| else: |
| warnings = [] |
|
|
| return { |
| "raw_text": str(data.get("raw_text", "")).strip(), |
| "medications": medications, |
| "warnings": warnings, |
| } |
|
|
|
|
| @spaces.GPU(duration=180) |
| def analyze_with_vl_model(image: Image.Image, task: str = "ocr") -> Any: |
| """ |
| λ¨μΌ VL λͺ¨λΈλ‘ λͺ¨λ μμ
μν |
| task: "ocr" (μ½λ΄ν¬ λΆμ) | "explain" (μ€λͺ
μμ±) | "image_prompt" (μ΄λ―Έμ§ ν둬ννΈ) |
| """ |
| try: |
| if task == "ocr": |
| |
| instructions = """μ¬μ§ μ μ½λ΄ν¬/μ²λ°©μ μ μ½κ³ JSON νμμΌλ‘ λ΅λ³νμΈμ.""" |
|
|
| schema = """{ |
| "raw_text": "OCRλ‘ μ½μ μ 체 λ¬Έμ₯", |
| "medications": [ |
| { |
| "name": "μ½ μ΄λ¦ (μνλͺ
κ³Ό μ±λΆλͺ
)", |
| "dose_per_intake": "1ν μ©λ", |
| "times_per_day": "ν루 λ³΅μ© νμ", |
| "time_slots": ["λ³΅μ© μκ°λ"], |
| "description": "μ½ μ€λͺ
", |
| "efficacy": "μ΄ μ½μ 무μμ
λκΉ? (μμΈν ν¨λ₯ν¨κ³Ό)", |
| "usage_precautions": "μ΄ μ½μ μ΄λ»κ² 볡μ©ν©λκΉ? (μμΈν 볡μ©λ²)", |
| "side_effects": "μ£Όμ λΆμμ©", |
| "drug_interactions": "μ½λ¬Ό μνΈμμ©", |
| "warnings": "νΉλ³ μ£Όμμ¬ν" |
| } |
| ], |
| "warnings": ["μ 체 κ²½κ³ "] |
| }""" |
|
|
| messages = [ |
| { |
| "role": "system", |
| "content": "λΉμ μ 20λ
κ²½λ ₯μ λνλ―Όκ΅ μμμ½μ¬μ
λλ€. μ½λ΄ν¬λ₯Ό μ λ°νκ² μ½κ³ μμ½νμ§(DUR) μμ€μ μ λ¬Έμ μ΄κ³ μμΈν μ 보λ₯Ό μ 곡ν©λλ€. λͺ¨λ νλλ₯Ό μ΅λν μμΈν μμ±νμΈμ.", |
| }, |
| { |
| "role": "user", |
| "content": [ |
| {"type": "text", "text": instructions}, |
| {"type": "text", "text": schema}, |
| {"type": "image"}, |
| ], |
| }, |
| ] |
|
|
| chat_text = VL_PROCESSOR.apply_chat_template(messages, add_generation_prompt=True) |
| inputs = VL_PROCESSOR(text=[chat_text], images=[image], return_tensors="pt").to(VL_MODEL.device) |
|
|
| output_ids = VL_MODEL.generate( |
| **inputs, |
| max_new_tokens=4096, |
| temperature=0.2, |
| top_p=0.9, |
| do_sample=True, |
| repetition_penalty=1.1, |
| ) |
|
|
| decoded = VL_PROCESSOR.batch_decode(output_ids, skip_special_tokens=False)[0] |
| assistant_text = _extract_assistant_content(decoded) |
| return _parse_vl_response(assistant_text) |
|
|
| elif task == "explain": |
| |
| return {"elderly_narrative": "", "child_narrative": "", "image_description": ""} |
|
|
| except Exception as e: |
| return {"error": str(e)} |
|
|
|
|
| def render_card(medications: List[Dict[str, Any]]) -> Image.Image: |
| """νλμ μΈ μ½λ¬Ό μΉ΄λ λ λλ§""" |
| try: |
| font_large = ImageFont.truetype("NotoSansKR-Regular.ttf", 28) |
| font_medium = ImageFont.truetype("NotoSansKR-Regular.ttf", 20) |
| font_small = ImageFont.truetype("NotoSansKR-Regular.ttf", 16) |
| except: |
| font_large = font_medium = font_small = None |
|
|
| if not medications: |
| canvas = Image.new("RGB", (900, 300), (255, 255, 255)) |
| draw = ImageDraw.Draw(canvas) |
| draw.text((350, 130), "μ½ μ λ³΄κ° μμ΅λλ€", fill=(140, 140, 140), font=font_medium) |
| return canvas |
|
|
| card_height_per_med = 240 |
| header_height = 120 |
| footer_height = 80 |
| total_height = header_height + (card_height_per_med * len(medications)) + footer_height |
|
|
| width = 900 |
| canvas = Image.new("RGB", (width, total_height), (248, 250, 252)) |
| draw = ImageDraw.Draw(canvas) |
|
|
| |
| for i in range(header_height): |
| alpha = i / header_height |
| color = ( |
| int(99 + (248 - 99) * alpha), |
| int(102 + (250 - 102) * alpha), |
| int(241 + (252 - 241) * alpha), |
| ) |
| draw.rectangle((0, i, width, i + 1), fill=color) |
|
|
| draw.text((40, 35), "π λ³΅μ© μλ΄", fill=(30, 41, 59), font=font_large) |
| draw.text((40, 75), f"{len(medications)}κ° μ½ν", fill=(71, 85, 105), font=font_small) |
|
|
| y = header_height + 30 |
|
|
| for idx, med in enumerate(medications): |
| card_y_start = y - 15 |
| card_y_end = y + 200 |
|
|
| |
| draw.rounded_rectangle( |
| (35, card_y_start + 5, width - 35, card_y_end + 5), |
| radius=16, |
| fill=(203, 213, 225), |
| ) |
|
|
| |
| draw.rounded_rectangle( |
| (30, card_y_start, width - 30, card_y_end), |
| radius=16, |
| fill=(255, 255, 255), |
| ) |
|
|
| |
| badge_x, badge_y = 45, y |
| draw.ellipse( |
| (badge_x, badge_y, badge_x + 45, badge_y + 45), |
| fill=(99, 102, 241), |
| ) |
| draw.text((badge_x + 12, badge_y + 8), str(idx + 1), fill=(255, 255, 255), font=font_medium) |
|
|
| |
| name_text = med.get("name", "μ½ μ΄λ¦ λ―ΈνμΈ") |
| draw.text((105, y + 8), name_text, fill=(15, 23, 42), font=font_medium) |
|
|
| y += 60 |
|
|
| |
| info_items = [ |
| ("π¦", "μ©λ", med.get('dose_per_intake', '-')), |
| ("π’", "νμ", f"{med.get('times_per_day', '-')}ν/μΌ"), |
| ("π", "μκ°", ", ".join(med.get('time_slots') or ["-"])), |
| ] |
|
|
| for icon, label, value in info_items: |
| draw.text((50, y), f"{icon} {label}", fill=(100, 116, 139), font=font_small) |
| draw.text((160, y), value, fill=(30, 41, 59), font=font_small) |
| y += 38 |
|
|
| y += 30 |
|
|
| |
| footer_y = total_height - footer_height + 25 |
| draw.text((40, footer_y), "β» λ³Έ μ±μ μ°Έκ³ μ©μ΄λ©°, μ€μ 볡μ½μ μμ¬Β·μ½μ¬μ μ§μλ₯Ό λ°λΌμ£ΌμΈμ.", |
| fill=(148, 163, 184), font=font_small) |
|
|
| return canvas |
|
|
|
|
| def medications_to_csv(medications: List[Dict[str, Any]]) -> str: |
| """CSV μμ±""" |
| if not medications: |
| return "" |
|
|
| rows = ["μ½λͺ
,1νμ©λ,1μΌνμ,μκ°λ"] |
| for med in medications: |
| row = [ |
| med.get("name", ""), |
| med.get("dose_per_intake", ""), |
| med.get("times_per_day", ""), |
| ";".join(med.get("time_slots") or []), |
| ] |
| rows.append(",".join(row)) |
|
|
| return "\n".join(rows) |
|
|
|
|
| def format_warnings(warnings: List[str]) -> str: |
| """κ²½κ³ λ©μμ§ ν¬λ§·""" |
| if not warnings: |
| return "β
μΈμλ μ λ³΄κ° μΆ©λΆν©λλ€." |
|
|
| lines = ["### β οΈ νμΈ νμ"] |
| for warn in warnings: |
| lines.append(f"- {warn}") |
| lines.append("\n> μλ£μ§μ μ§μκ° κ°μ₯ μ νν©λλ€.") |
| return "\n".join(lines) |
|
|
|
|
| @spaces.GPU(duration=120) |
| def generate_full_explanation(medications: List[Dict[str, Any]], raw_text: str, web_info: str = "") -> Dict[str, str]: |
| """VL λͺ¨λΈλ‘ μ€λͺ
μμ±""" |
| try: |
| med_summary = "\n".join([ |
| f"- {med.get('name')} {med.get('dose_per_intake')} (ν루 {med.get('times_per_day')}ν)" |
| for med in medications |
| ]) |
|
|
| web_context = f"\n\nμΉ κ²μ¦: {web_info}" if web_info else "" |
|
|
| prompt = f"""λ€μ μ½λ¬Ό μ 보λ₯Ό λ°νμΌλ‘ μ΄λ₯΄μ κ³Ό μ΄λ¦°μ΄λ₯Ό μν μ€λͺ
μ μμ±νμΈμ. |
| |
| μ½ μ 보: |
| {med_summary} |
| |
| μλ¬Έ: {raw_text}{web_context} |
| |
| JSON νμμΌλ‘ λ΅λ³: |
| {{ |
| "elderly": {{ |
| "narrative": "μ΄λ₯΄μ μ μν μ€λͺ
(μ‘΄λλ§, ꡬ체μ , 5-7λ¬Έμ₯)", |
| "image_description": "μ½ λ³΅μ© μ₯λ©΄ λ¬μ¬ (νκ΅μ΄)" |
| }}, |
| "child": {{ |
| "narrative": "μ΄λ¦°μ΄λ₯Ό μν μ€λͺ
(μ¬μ΄ λ§, μ¬λ―Έμκ², 4-6λ¬Έμ₯)", |
| "image_description": "μ½ λ³΅μ© μ₯λ©΄ λ¬μ¬ (νκ΅μ΄)" |
| }} |
| }}""" |
|
|
| messages = [ |
| { |
| "role": "system", |
| "content": "λΉμ μ 20λ
κ²½λ ₯ μμμ½μ¬μ
λλ€. νμ κ΅μ‘ μ λ¬Έκ°μ
λλ€.", |
| }, |
| { |
| "role": "user", |
| "content": prompt, |
| }, |
| ] |
|
|
| chat_text = VL_PROCESSOR.apply_chat_template(messages, add_generation_prompt=True) |
| inputs = VL_PROCESSOR(text=[chat_text], images=None, return_tensors="pt").to(VL_MODEL.device) |
|
|
| output_ids = VL_MODEL.generate( |
| **inputs, |
| max_new_tokens=2560, |
| temperature=0.7, |
| top_p=0.9, |
| do_sample=True, |
| repetition_penalty=1.15, |
| ) |
|
|
| decoded = VL_PROCESSOR.batch_decode(output_ids, skip_special_tokens=False)[0] |
| text = _extract_assistant_content(decoded) |
|
|
| json_block = _extract_json_block(text) |
| if json_block: |
| data = json.loads(json_block) |
| elderly = data.get("elderly", {}) |
| child = data.get("child", {}) |
|
|
| return { |
| "elderly_narrative": str(elderly.get("narrative", "")).strip(), |
| "child_narrative": str(child.get("narrative", "")).strip(), |
| } |
|
|
| return { |
| "elderly_narrative": "μ€λͺ
μ μμ±νμ§ λͺ»νμ΅λλ€.", |
| "child_narrative": "μ€λͺ
μ μμ±νμ§ λͺ»νμ΅λλ€.", |
| } |
|
|
| except Exception as e: |
| return { |
| "elderly_narrative": "μ€λͺ
μμ± μ€ μ€λ₯ λ°μ", |
| "child_narrative": "μ€λͺ
μμ± μ€ μ€λ₯ λ°μ", |
| } |
|
|
|
|
| def run_pipeline(image: Optional[Image.Image], progress=gr.Progress()): |
| """λ©μΈ νμ΄νλΌμΈ""" |
| if image is None: |
| return ( |
| "μ΄λ―Έμ§λ₯Ό μ
λ‘λνμΈμ.", |
| None, |
| None, |
| "μ΄λ―Έμ§λ₯Ό λ¨Όμ μ
λ‘λν΄ μ£ΌμΈμ.", |
| "π· μ½ λ΄ν¬ μ¬μ§μ μ¬λ¦¬λ©΄ μΈμμ΄ μμλ©λλ€.", |
| "", |
| "μ½λ¬Ό μ λ³΄κ° νμλ©λλ€.", |
| ) |
|
|
| progress(0, desc="π μ½λ΄ν¬ μ΄λ―Έμ§ λΆμ μ€...") |
| result = analyze_with_vl_model(image, task="ocr") |
|
|
| medications = result.get("medications") or [] |
|
|
| |
| progress(0.25, desc="π μΉμμ μ½λ¬Ό μ 보 κ²μ¦ μ€...") |
| web_info_results = [] |
| for med in medications[:3]: |
| drug_name = med.get("name", "") |
| if drug_name: |
| web_info = search_drug_web_simple(drug_name) |
| if web_info: |
| web_info_results.append(web_info) |
| med["web_verified"] = True |
|
|
| web_search_info = "\n".join(web_info_results) if web_info_results else "" |
|
|
| progress(0.5, desc="π¬ μ€λͺ
μμ± μ€...") |
| narratives = generate_full_explanation(medications, result.get("raw_text", ""), web_search_info) |
|
|
| progress(0.75, desc="π¨ μΉ΄λ λ λλ§ μ€...") |
| card_img = render_card(medications) |
| csv_row = medications_to_csv(medications) |
|
|
| |
| detailed_info = "# π μ½λ¬Ό μμΈ μ 보\n\n" |
|
|
| if web_search_info: |
| detailed_info += "β
**μΉ κ²μ¦ μλ£**\n\n" |
| detailed_info += f"> {web_search_info}\n\n---\n\n" |
|
|
| for idx, med in enumerate(medications): |
| web_badge = " π" if med.get("web_verified") else "" |
| detailed_info += f"## {idx + 1}. {med.get('name', 'μ½ μ΄λ¦ λ―ΈνμΈ')}{web_badge}\n\n" |
|
|
| if med.get("efficacy"): |
| detailed_info += f"### π μ΄ μ½μ 무μμ
λκΉ?\n{med.get('efficacy')}\n\n" |
|
|
| if med.get("usage_precautions"): |
| detailed_info += f"### π μ΄ μ½μ μ΄λ»κ² 볡μ©ν©λκΉ?\n{med.get('usage_precautions')}\n\n" |
|
|
| if med.get("side_effects"): |
| detailed_info += f"### β οΈ λΆμμ©\n{med.get('side_effects')}\n\n" |
|
|
| if med.get("drug_interactions"): |
| detailed_info += f"### π μ½λ¬Ό μνΈμμ©\n{med.get('drug_interactions')}\n\n" |
|
|
| if med.get("warnings"): |
| detailed_info += f"### β‘ νΉλ³ μ£Όμμ¬ν\n{med.get('warnings')}\n\n" |
|
|
| detailed_info += "---\n\n" |
|
|
| |
| markdown = ( |
| "## π΄ μ΄λ₯΄μ μ μν μ€λͺ
\n\n" |
| + (narratives.get("elderly_narrative") or "μ€λͺ
μ μ€λΉνμ§ λͺ»νμ΅λλ€.") |
| + "\n\n## πΆ μ΄λ¦°μ΄λ₯Ό μν μ€λͺ
\n\n" |
| + (narratives.get("child_narrative") or "μ€λͺ
μ μ€λΉνμ§ λͺ»νμ΅λλ€.") |
| + "\n\n> π‘ νμ μλ£μ§μ μλ΄λ₯Ό μ°μ νμΈμ." |
| ) |
|
|
| warnings_md = format_warnings(result.get("warnings", [])) |
| raw_text = result.get("raw_text", "") |
| json_text = json.dumps(result, ensure_ascii=False, indent=2) |
|
|
| progress(1.0, desc="β
μλ£!") |
| return json_text, card_img, csv_row, markdown, warnings_md, raw_text, detailed_info |
|
|
|
|
| |
| CUSTOM_CSS = """ |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); |
| |
| :root { |
| --primary: #6366f1; |
| --primary-dark: #4f46e5; |
| --secondary: #8b5cf6; |
| --success: #10b981; |
| --warning: #f59e0b; |
| --danger: #ef4444; |
| --gray-50: #f9fafb; |
| --gray-100: #f3f4f6; |
| --gray-200: #e5e7eb; |
| --gray-300: #d1d5db; |
| --gray-600: #4b5563; |
| --gray-800: #1f2937; |
| --gray-900: #111827; |
| } |
| |
| body { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| } |
| |
| .gradio-container { |
| max-width: 1400px !important; |
| margin: auto; |
| background: rgba(255, 255, 255, 0.95); |
| border-radius: 24px; |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); |
| padding: 40px; |
| } |
| |
| .hero-section { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| border-radius: 20px; |
| padding: 50px 40px; |
| margin-bottom: 40px; |
| color: white; |
| box-shadow: 0 20px 40px -10px rgba(102, 126, 234, 0.4); |
| } |
| |
| .hero-section h1 { |
| font-size: 2.5rem; |
| font-weight: 700; |
| margin-bottom: 16px; |
| text-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
| } |
| |
| .hero-section p { |
| font-size: 1.15rem; |
| opacity: 0.95; |
| line-height: 1.6; |
| } |
| |
| .card { |
| background: white; |
| border-radius: 16px; |
| padding: 32px; |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
| transition: all 0.3s ease; |
| } |
| |
| .card:hover { |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); |
| transform: translateY(-2px); |
| } |
| |
| .primary-btn button { |
| background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%) !important; |
| border: none !important; |
| color: white !important; |
| font-weight: 600 !important; |
| font-size: 1.05rem !important; |
| padding: 16px 32px !important; |
| border-radius: 12px !important; |
| box-shadow: 0 10px 20px -5px rgba(99, 102, 241, 0.4) !important; |
| transition: all 0.3s ease !important; |
| } |
| |
| .primary-btn button:hover { |
| transform: translateY(-2px) !important; |
| box-shadow: 0 15px 30px -5px rgba(99, 102, 241, 0.5) !important; |
| } |
| |
| .tab-nav button { |
| font-weight: 500 !important; |
| border-radius: 8px !important; |
| transition: all 0.2s ease !important; |
| } |
| |
| .tab-nav button.selected { |
| background: var(--primary) !important; |
| color: white !important; |
| } |
| |
| .notice { |
| background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); |
| border-left: 4px solid var(--warning); |
| border-radius: 12px; |
| padding: 20px; |
| color: var(--gray-800); |
| } |
| |
| .output-card { |
| background: var(--gray-50); |
| border-radius: 16px; |
| padding: 28px; |
| border: 1px solid var(--gray-200); |
| } |
| |
| .gr-image { |
| border-radius: 16px !important; |
| box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.1) !important; |
| } |
| |
| .csv-box textarea { |
| font-family: 'JetBrains Mono', 'Courier New', monospace !important; |
| font-size: 0.9rem !important; |
| background: var(--gray-900) !important; |
| color: #10b981 !important; |
| border-radius: 12px !important; |
| } |
| |
| .accordion { |
| border-radius: 12px !important; |
| border: 1px solid var(--gray-200) !important; |
| } |
| |
| h1, h2, h3 { |
| font-weight: 600; |
| color: var(--gray-900); |
| } |
| |
| .markdown-text { |
| line-height: 1.8; |
| color: var(--gray-800); |
| } |
| """ |
|
|
| HERO_HTML = """ |
| <div class="hero-section"> |
| <h1>π₯ MedCard Pro</h1> |
| <p> |
| <strong>AI κΈ°λ° μ€λ§νΈ μ½λ¬Ό κ΄λ¦¬ μμ€ν
</strong><br> |
| Qwen2-VL-7B (8λΉνΈ μ΅μ ν + Ultra Quality μΆλ‘ )κ° μ½λ΄ν¬λ₯Ό μ ννκ² λΆμνκ³ ,<br> |
| μΉμμ μ€μκ°μΌλ‘ μ 보λ₯Ό κ²μ¦νμ¬ νλ‘νμ
λν λ³΅μ½ μλ΄λ₯Ό μ 곡ν©λλ€. |
| </p> |
| </div> |
| """ |
|
|
| |
| with gr.Blocks(theme=gr.themes.Soft(), css=CUSTOM_CSS) as demo: |
| gr.HTML(HERO_HTML) |
|
|
| with gr.Row(): |
| with gr.Column(scale=5, elem_classes=["card"]): |
| gr.Markdown("### πΈ μ½ λ΄ν¬ μ¬μ§ μ
λ‘λ") |
| img_in = gr.Image(type="pil", label="μ½λ΄ν¬/μ²λ°©μ μ¬μ§", height=400) |
| warn_md = gr.Markdown("π‘ μ½ λ΄ν¬ μ¬μ§μ μ¬λ €μ£ΌμΈμ. AIκ° μλμΌλ‘ λΆμν©λλ€.", elem_classes=["notice"]) |
| btn = gr.Button("π λΆμ μμ", elem_classes=["primary-btn"], size="lg") |
|
|
| with gr.Column(scale=7, elem_classes=["card"]): |
| gr.Markdown("### π λΆμ κ²°κ³Ό") |
|
|
| with gr.Tabs(): |
| with gr.Tab("π μ½λ¬Ό μμΈ μ 보"): |
| detailed_info_md = gr.Markdown("λΆμμ μμνλ©΄ μ¬κΈ°μ μ½λ¬Ό μ λ³΄κ° νμλ©λλ€.", elem_classes=["output-card"]) |
|
|
| with gr.Tab("π₯ μ¬μ΄ μ€λͺ
"): |
| explain_md = gr.Markdown("μ΄λ₯΄μ κ³Ό μ΄λ¦°μ΄λ₯Ό μν μ€λͺ
μ΄ νμλ©λλ€.", elem_classes=["output-card"]) |
|
|
| with gr.Tab("π
λ³΅μ© μΌμ "): |
| card_out = gr.Image(type="pil", label="μΌμ μΉ΄λ") |
|
|
| with gr.Accordion("π μμΈ λΆμ κ²°κ³Ό", open=False): |
| raw_box = gr.Textbox(label="OCR μλ¬Έ", lines=4, interactive=False) |
| csv_box = gr.Textbox(label="CSV λ°μ΄ν°", lines=3, elem_classes=["csv-box"]) |
| json_out = gr.Code(label="JSON λ°μ΄ν°", language="json") |
|
|
| btn.click( |
| run_pipeline, |
| inputs=img_in, |
| outputs=[json_out, card_out, csv_box, explain_md, warn_md, raw_box, detailed_info_md], |
| ) |
|
|
| gr.Markdown( |
| """ |
| --- |
| |
| ### βΉοΈ μ£Όμμ¬ν |
| |
| μ΄ μλΉμ€λ **μ°Έκ³ μ© λꡬ**μ
λλ€. μ€μ 볡μ½μ λ°λμ **μμ¬Β·μ½μ¬μ μ§μ**μ λ°λΌμ£ΌμΈμ. |
| |
| π κ°μΈμ 보λ μ μ₯λμ§ μμΌλ©°, λͺ¨λ μ²λ¦¬λ μ€μκ°μΌλ‘ μ΄λ£¨μ΄μ§λλ€. |
| """ |
| ) |
|
|
| if __name__ == "__main__": |
| demo.queue().launch() |