MedCard / app.py
LLDDWW's picture
sdfdsfads23
ea6e0a5
raw
history blame
24.5 kB
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,
)
# 졜고 ν’ˆμ§ˆ 곡개 λͺ¨λΈ + 8λΉ„νŠΈ μ–‘μžν™” (ZeroGPU μ΅œμ ν™”)
# Note: 32B/72BλŠ” gated model(인증 ν•„μš”), 7Bκ°€ μ΅œλŒ€ 곡개 λͺ¨λΈ
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
# 8λΉ„νŠΈ μ–‘μžν™” + FP16 ν˜Όν•© μ •λ°€λ„λ‘œ 졜고 μ„±λŠ₯
model = Qwen2VLForConditionalGeneration.from_pretrained(
VL_MODEL_ID,
device_map=device_map,
load_in_8bit=True, # 8λΉ„νŠΈ μ–‘μžν™”λ‘œ λ©”λͺ¨λ¦¬ 50% 절감
torch_dtype=torch.float16, # Mixed precision (ν’ˆμ§ˆ μœ μ§€, 속도 ν–₯상)
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) # κ³ ν’ˆμ§ˆ 좔둠을 μœ„ν•œ 3λΆ„ ν—ˆμš©
def analyze_with_vl_model(image: Image.Image, task: str = "ocr") -> Any:
"""
단일 VL λͺ¨λΈλ‘œ λͺ¨λ“  μž‘μ—… μˆ˜ν–‰
task: "ocr" (μ•½λ΄‰νˆ¬ 뢄석) | "explain" (μ„€λͺ… 생성) | "image_prompt" (이미지 ν”„λ‘¬ν”„νŠΈ)
"""
try:
if task == "ocr":
# μ•½λ΄‰νˆ¬ 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":
# μ„€λͺ… 생성 (imageλŠ” None, text만 μ‚¬μš©)
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
# ν˜„λŒ€μ μΈ CSS
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>
"""
# Gradio μΈν„°νŽ˜μ΄μŠ€
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()