File size: 8,321 Bytes
76e08e0
 
64c57fb
f16cb1a
76e08e0
 
69a39af
7ce0e26
f16cb1a
72114b8
f16cb1a
 
 
 
e53f54d
 
459e392
98b216f
459e392
 
 
 
 
 
 
 
98b216f
459e392
 
 
 
 
 
64c57fb
f16cb1a
459e392
f16cb1a
 
64c57fb
f16cb1a
 
64c57fb
f16cb1a
 
 
 
 
 
 
64c57fb
 
 
 
 
 
92bb45b
64c57fb
92bb45b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64c57fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92bb45b
 
 
 
64c57fb
 
 
 
 
 
 
 
98b216f
7ce0e26
98b216f
 
64c57fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e53f54d
ad98685
64c57fb
 
 
 
3cec557
64c57fb
 
 
 
 
 
 
3cec557
64c57fb
ad98685
 
7ce0e26
39446f7
7ce0e26
39446f7
64c57fb
39446f7
ad98685
39446f7
 
 
 
 
 
6d9d526
 
7ce0e26
6d9d526
98b216f
 
 
 
 
 
 
 
 
7ce0e26
98b216f
 
 
7ce0e26
98b216f
7ce0e26
98b216f
7ce0e26
98b216f
 
 
7ce0e26
 
 
98b216f
 
 
7ce0e26
98b216f
 
7ce0e26
98b216f
 
7ce0e26
98b216f
 
7ce0e26
 
98b216f
 
 
7ce0e26
98b216f
 
7ce0e26
 
 
98b216f
 
7ce0e26
 
 
 
 
 
98b216f
 
7ce0e26
 
98b216f
 
7ce0e26
 
98b216f
7ce0e26
 
98b216f
 
 
7ce0e26
98b216f
7ce0e26
98b216f
 
 
 
6d9d526
 
 
 
7ce0e26
 
 
6d9d526
 
 
98b216f
6d9d526
 
98b216f
7ce0e26
 
 
39446f7
98b216f
39446f7
 
 
98b216f
7ce0e26
 
 
39446f7
6d9d526
 
7ce0e26
 
5f1a404
39446f7
f16cb1a
7ce0e26
5f1a404
 
7ce0e26
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
import json
import re
from typing import List, Optional, Tuple
import numpy as np

import gradio as gr
import spaces
from PIL import Image
from paddleocr import PaddleOCR

# PaddleOCR ์ดˆ๊ธฐํ™” (ํ•œ๊ตญ์–ด)
print("๐Ÿ”„ Loading PaddleOCR (Korean)...")
OCR_MODEL = PaddleOCR(use_angle_cls=True, lang='korean', use_gpu=True)
print("โœ… PaddleOCR loaded!")


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 extract_text_from_image(image: Image.Image) -> str:
    """PaddleOCR๋กœ ์ด๋ฏธ์ง€์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ"""
    try:
        # PIL Image๋ฅผ numpy array๋กœ ๋ณ€ํ™˜
        img_array = np.array(image)

        # PaddleOCR ์‹คํ–‰
        result = OCR_MODEL.ocr(img_array, cls=True)

        # ๊ฒฐ๊ณผ์—์„œ ํ…์ŠคํŠธ๋งŒ ์ถ”์ถœ
        if result and result[0]:
            texts = [line[1][0] for line in result[0]]
            extracted_text = "\n".join(texts)
            return extracted_text.strip()
        else:
            return "ํ…์ŠคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."

    except Exception as e:
        raise Exception(f"OCR ์˜ค๋ฅ˜: {str(e)}")


def extract_medications_from_text(text: str) -> List[str]:
    """Stage 2: Qwen2.5๋กœ ํ…์ŠคํŠธ์—์„œ ์•ฝ ์ด๋ฆ„๋งŒ ์ถ”์ถœ"""
    try:
        messages = [
            {
                "role": "system",
                "content": "You are a medical text analyzer. Extract only medication names from the given text and return them as a JSON array. Return ONLY valid JSON format."
            },
            {
                "role": "user",
                "content": f"Extract all medication names from this text:\n\n{text}\n\nReturn format: {{\"medications\": [\"name1\", \"name2\"]}}"
            }
        ]

        prompt = LLM_TOKENIZER.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True
        )

        inputs = LLM_TOKENIZER(prompt, return_tensors="pt").to(LLM_MODEL.device)

        with torch.no_grad():
            outputs = LLM_MODEL.generate(
                **inputs,
                max_new_tokens=512,
                temperature=0.3,
                top_p=0.9,
                do_sample=True,
                pad_token_id=LLM_TOKENIZER.eos_token_id,
            )

        response = LLM_TOKENIZER.decode(outputs[0], skip_special_tokens=True)

        # Extract assistant response (Qwen format)
        if "<|im_start|>assistant" in response:
            response = response.split("<|im_start|>assistant")[-1]
            response = response.replace("<|im_end|>", "").strip()

        # Parse JSON
        json_match = re.search(r'\{.*?\}', response, re.DOTALL)
        if json_match:
            data = json.loads(json_match.group(0))
            medications = data.get("medications", [])
            if isinstance(medications, list) and medications:
                return [str(m).strip() for m in medications if str(m).strip()]

        return ["์•ฝ ์ด๋ฆ„์„ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."]

    except Exception as e:
        raise Exception(f"LLM ๋ถ„์„ ์˜ค๋ฅ˜: {str(e)}")


@spaces.GPU(duration=120)
def extract_medication_names(image: Image.Image) -> Tuple[str, List[str]]:
    """2๋‹จ๊ณ„ ํŒŒ์ดํ”„๋ผ์ธ: OCR โ†’ LLM ๋ถ„์„"""
    try:
        # Stage 1: OCR๋กœ ํ…์ŠคํŠธ ์ถ”์ถœ
        extracted_text = extract_text_from_image(image)

        if not extracted_text:
            return "", ["ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."]

        # Stage 2: LLM์œผ๋กœ ์•ฝ ์ด๋ฆ„ ์ถ”์ถœ
        medications = extract_medications_from_text(extracted_text)

        return extracted_text, medications

    except Exception as e:
        return "", [f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"]


def format_results(extracted_text: str, medications: List[str]) -> Tuple[str, str]:
    """๊ฒฐ๊ณผ๋ฅผ ํฌ๋งทํŒ…"""
    # ์ถ”์ถœ๋œ ์ „์ฒด ํ…์ŠคํŠธ
    text_output = f"### ๐Ÿ“„ ์ถ”์ถœ๋œ ํ…์ŠคํŠธ\n\n```\n{extracted_text}\n```"

    # ์•ฝ ์ด๋ฆ„ ๋ฆฌ์ŠคํŠธ
    if not medications or medications[0].startswith("์˜ค๋ฅ˜") or medications[0].startswith("์•ฝ ์ด๋ฆ„์„ ์ฐพ์ง€") or medications[0].startswith("ํ…์ŠคํŠธ๋ฅผ"):
        med_output = f"### โš ๏ธ {medications[0] if medications else '์•ฝ ์ด๋ฆ„์„ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'}"
    else:
        med_output = f"### ๐Ÿ’Š ๊ฒ€์ถœ๋œ ์•ฝ๋ฌผ ({len(medications)}๊ฐœ)\n\n"
        for idx, med_name in enumerate(medications, 1):
            med_output += f"{idx}. **{med_name}**\n"

    return text_output, med_output


def run_analysis(image: Optional[Image.Image], progress=gr.Progress()):
    """๋ฉ”์ธ ๋ถ„์„ ํŒŒ์ดํ”„๋ผ์ธ: OCR๋งŒ ์‹คํ–‰"""
    if image is None:
        return "๐Ÿ“ท ์•ฝ ๋ด‰ํˆฌ๋‚˜ ์ฒ˜๋ฐฉ์ „ ์‚ฌ์ง„์„ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”."

    progress(0.5, desc="๐Ÿ“ธ OCR ํ…์ŠคํŠธ ์ถ”์ถœ ์ค‘...")

    try:
        extracted_text = extract_text_from_image(image)
        progress(1.0, desc="โœ… ์™„๋ฃŒ!")
        return f"### ๐Ÿ“„ OCR ์ถ”์ถœ ๊ฒฐ๊ณผ\n\n```\n{extracted_text}\n```"
    except Exception as e:
        return f"### โš ๏ธ ์˜ค๋ฅ˜ ๋ฐœ์ƒ\n\n{str(e)}"


# ์‹ฌํ”Œํ•œ CSS
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

:root {
    --primary: #6366f1;
    --secondary: #8b5cf6;
}

body {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}

.gradio-container {
    max-width: 900px !important;
    margin: auto;
    background: rgba(255, 255, 255, 0.98);
    border-radius: 24px;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.3);
    padding: 40px;
}

.hero {
    text-align: center;
    padding: 30px 20px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 20px;
    color: white;
    margin-bottom: 30px;
}

.hero h1 {
    font-size: 2.5rem;
    font-weight: 700;
    margin-bottom: 10px;
}

.hero p {
    font-size: 1.1rem;
    opacity: 0.95;
}

.upload-section {
    background: white;
    border-radius: 16px;
    padding: 30px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
    margin-bottom: 20px;
}

.result-section {
    background: white;
    border-radius: 16px;
    padding: 30px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
    min-height: 200px;
}

.analyze-btn button {
    background: linear-gradient(135deg, var(--primary), var(--secondary)) !important;
    color: white !important;
    font-weight: 600 !important;
    font-size: 1.1rem !important;
    padding: 18px 40px !important;
    border-radius: 12px !important;
    border: none !important;
    box-shadow: 0 10px 20px -5px rgba(99, 102, 241, 0.5) !important;
    transition: all 0.3s ease !important;
}

.analyze-btn button:hover {
    transform: translateY(-2px) !important;
    box-shadow: 0 15px 30px -5px rgba(99, 102, 241, 0.6) !important;
}

.gr-image {
    border-radius: 12px !important;
}
"""

HERO_HTML = """
<div class="hero">
    <h1>๐Ÿ’Š ์•ฝ ์ด๋ฆ„ ์ถ”์ถœ๊ธฐ</h1>
    <p>์•ฝ๋ด‰ํˆฌ/์ฒ˜๋ฐฉ์ „ ์‚ฌ์ง„์—์„œ ์•ฝ ์ด๋ฆ„์„ ์ž๋™์œผ๋กœ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค</p>
</div>
"""

# Gradio ์ธํ„ฐํŽ˜์ด์Šค
with gr.Blocks(theme=gr.themes.Soft(), css=CUSTOM_CSS) as demo:
    gr.HTML(HERO_HTML)

    with gr.Column(elem_classes=["upload-section"]):
        gr.Markdown("### ๐Ÿ“ธ ์‚ฌ์ง„ ์—…๋กœ๋“œ")
        image_input = gr.Image(type="pil", label="์•ฝ๋ด‰ํˆฌ ๋˜๋Š” ์ฒ˜๋ฐฉ์ „ ์‚ฌ์ง„", height=350)
        analyze_button = gr.Button("๐Ÿ” OCR ํ…์ŠคํŠธ ์ถ”์ถœ", elem_classes=["analyze-btn"], size="lg")

    with gr.Column(elem_classes=["result-section"]):
        gr.Markdown("### ๐Ÿ“‹ OCR ์ถ”์ถœ ๊ฒฐ๊ณผ")
        text_output = gr.Markdown("OCR๋กœ ์ถ”์ถœ๋œ ์ „์ฒด ํ…์ŠคํŠธ๊ฐ€ ์—ฌ๊ธฐ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.")

    analyze_button.click(
        run_analysis,
        inputs=image_input,
        outputs=text_output,
    )

    gr.Markdown("""
    ---

    **โ„น๏ธ OCR ๋ชจ๋ธ**
    - PaddleOCR (Korean) - ํ•œ๊ตญ์–ด ํ…์ŠคํŠธ ์ธ์‹์— ์ตœ์ ํ™”๋œ OCR ์—”์ง„
    """)

if __name__ == "__main__":
    demo.queue().launch()