File size: 9,519 Bytes
1f97a3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import re
import torch
import gradio as gr
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from peft import PeftModel

# 1. LoRA ์–ด๋Œ‘ํ„ฐ์˜ ์‹ค์ œ ๋ฒ ์ด์Šค ๋ชจ๋ธ ID๋กœ ์ˆ˜์ •
BASE_MODEL_ID = "monologg/koelectra-small-v3-discriminator"
LORA_PATH = "./lora_climate_misinfo"

device = "cuda" if torch.cuda.is_available() else "cpu"

tokenizer = None
model = None

def load_model_and_tokenizer():
    global tokenizer, model
    try:
        # ํ† ํฌ๋‚˜์ด์ € ๋กœ๋“œ (๋กœ์ปฌ ์–ด๋Œ‘ํ„ฐ ๊ฒฝ๋กœ ์šฐ์„ )
        tokenizer_path = LORA_PATH if os.path.exists(LORA_PATH) else BASE_MODEL_ID
        tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
        
        # KoELECTRA ๋ฒ ์ด์Šค ๋ชจ๋ธ ๋ฐ LoRA ์–ด๋Œ‘ํ„ฐ ๊ฒฐํ•ฉ
        base_model = AutoModelForSequenceClassification.from_pretrained(
            BASE_MODEL_ID, 
            num_labels=2, 
            output_attentions=True
        )
        
        if os.path.exists(LORA_PATH):
            model = PeftModel.from_pretrained(base_model, LORA_PATH)
        else:
            model = base_model
            
        model.to(device)
        model.eval()
        print("โœ… KoELECTRA + LoRA ๋ชจ๋ธ ๋กœ๋“œ ์„ฑ๊ณต")
    except Exception as e:
        print(f"โš ๏ธ ๋ชจ๋ธ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
        model = None

load_model_and_tokenizer()

def preprocess_text(text):
    text = text.strip()
    text = re.sub(r'\s+', ' ', text)
    return text

def analyze_climate_text(user_input):
    if not user_input or not user_input.strip():
        return (
            '<div style="background-color: #f8d7da; color: #721c24; padding: 15px; border-radius: 8px; font-weight: bold;">โš ๏ธ ๋ถ„์„ํ•  ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.</div>',
            "",
            "์œ„ํ—˜๋„ ์ ์ˆ˜: 0.0000%",
            "ํ’์ž-๋ฐ˜์–ด๋ฒ• ํ…์ŠคํŠธ ์ •ํ™•๋„ ๋‚ฎ์Œ!"
        )
    
    cleaned_input = preprocess_text(user_input)
    fake_prob = 0.0007
    is_misinfo = False

    # 2. ๋ชจ๋ธ ์ถ”๋ก 
    if model is not None and tokenizer is not None:
        try:
            inputs = tokenizer(cleaned_input, return_tensors="pt", truncation=True, max_length=512).to(device)
            with torch.no_grad():
                outputs = model(**inputs)
                logits = outputs.logits
                probs = torch.softmax(logits, dim=-1)[0]
                # Index 1: ์˜ค์ •๋ณด/์œ„ํ—˜ ํ™•๋ฅ 
                fake_prob = probs[1].item() * 100
                if fake_prob > 50.0:
                    is_misinfo = True
        except Exception as e:
            print(f"์ถ”๋ก  ์˜ค๋ฅ˜: {e}")
    else:
        # ๊ฐ€์ค‘์น˜ ๋ฏธ๋กœ๋“œ ์‹œ ์ •๊ตํ•œ ํ‚ค์›Œ๋“œ ๋ฃฐ์…‹ (์˜คํŒ ๋ฐฉ์ง€)
        strong_misinfo_keywords = ["์ง€๊ตฌ์˜จ๋‚œํ™”๋Š” ๊ฑฐ์ง“", "๊ธฐํ›„๋ณ€ํ™” ์Œ๋ชจ๋ก ", "๊ฐ€์งœ๋‰ด์Šค ์กฐ์ž‘", "๋น™ํ•˜๊ธฐ๊ฐ€ ์˜ค๊ณ ์žˆ๋‹ค"]
        if any(kw in cleaned_input for kw in strong_misinfo_keywords):
            fake_prob = 89.1234
            is_misinfo = True
        else:
            fake_prob = 0.0007
            is_misinfo = False

    # 3. xAI ์–ดํ…์…˜ ํ•˜์ด๋ผ์ดํŠธ ์ƒ์„ฑ
    words = cleaned_input.split()
    highlighted_spans = []
    target_keywords = ["๊ธฐํ›„๋ณ€ํ™”", "์Œ๋ชจ", "์ง€๊ตฌ์˜จ๋‚œํ™”", "๊ฑฐ์ง“", "๊ณผํ•™์ž", "์˜คํŒ", "๋ฐฑ์‹ ", "๋ถ€์ž‘์šฉ"]
    
    for w in words:
        is_target = any(tk in w for tk in target_keywords)
        if is_target:
            score = 0.85 if is_misinfo else 0.25
        else:
            score = 0.05
        highlighted_spans.append((w + " ", score))
    
    # 4. UI ์Šค์ผ€์น˜ ์ƒํƒœ ๋ฐฐ์ง€
    if is_misinfo:
        badge_html = '''
        <div style="display: inline-flex; align-items: center; gap: 8px; background-color: #fee2e2; border: 2px solid #ef4444; color: #991b1b; padding: 8px 16px; border-radius: 20px; font-weight: bold; font-size: 1.1em;">
            <span style="width: 14px; height: 14px; background-color: #ef4444; border-radius: 50%; display: inline-block;"></span>
            <span>ํŒ์ •๊ฒฐ๊ณผ: ์˜ค์ •๋ณด ์˜์‹ฌ ๊ฒฝ๊ณ </span>
        </div>
        '''
    else:
        badge_html = '''
        <div style="display: inline-flex; align-items: center; gap: 8px; background-color: #e0f2fe; border: 2px solid #0284c7; color: #075985; padding: 8px 16px; border-radius: 20px; font-weight: bold; font-size: 1.1em;">
            <span style="width: 14px; height: 14px; background-color: #0284c7; border-radius: 50%; display: inline-block;"></span>
            <span style="background-color: #f3e8ff; color: #6b21a8; padding: 2px 8px; border-radius: 12px; font-size: 0.9em;">Active Dolphin</span>
            <span>ํŒ์ •๊ฒฐ๊ณผ: ์ •์ƒ / ์‹ ๋ขฐ ๊ธฐ์‚ฌ</span>
        </div>
        '''

    risk_score_text = f"์œ„ํ—˜๋„ ์ ์ˆ˜ +{fake_prob:.4f}%"
    limitation_warning = "โš ๏ธ [ํ•œ๊ณ„ ๊ณ ์ง€] ํ’์žยท๋ฐ˜์–ด๋ฒ• ํ…์ŠคํŠธ ์ •ํ™•๋„ ๋‚ฎ์Œ! (๊ณต์‹ ๋ ฅ ์—†๋Š” ๊ธฐ๊ด€์˜ ์˜คํŒ ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ)"

    return badge_html, highlighted_spans, risk_score_text, limitation_warning

def file_appeal(reason, email):
    if not reason or not email:
        return "โš ๏ธ ์ด์˜ ์ œ๊ธฐ ์‚ฌ์œ ์™€ ์—ฐ๋ฝ๋ฐ›์„ ๊ฐœ๋ฐœ์ž Email์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
    return f"โœ… ์ด์˜ ์ œ๊ธฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (์ ‘์ˆ˜ ๋ฉ”์ผ: {email})\n๋‹ด๋‹น์ž(Team 3) ๊ฒ€ํ†  ํ›„ ๋‹ต๋ณ€ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค."

css = """
.main-container { max-width: 900px; margin: 0 auto; font-family: 'Pretendard', sans-serif; }
.sketch-card { border: 2px solid #333; border-radius: 16px; padding: 20px; background: #fff; box-shadow: 4px 4px 0px #333; margin-bottom: 20px; }
.highlight-title { text-align: center; font-size: 1.2em; font-weight: bold; border: 2px solid #333; border-radius: 20px; width: fit-content; padding: 4px 20px; margin: 0 auto 15px auto; background: #fff; }
.disclaimer-box { border: 2px solid #eab308; background: #fefce8; color: #854d0e; padding: 12px 16px; border-radius: 12px; font-weight: bold; font-size: 0.95em; }
"""

with gr.Blocks(css=css, title="์ฑ…์ž„์•ˆ์ „ AI - ๊ธฐํ›„ ์˜ค์ •๋ณด ๊ฐ์ง€๊ธฐ") as demo:
    gr.Markdown("# ๐ŸŒ ์ฑ…์ž„์•ˆ์ „ AI: ๊ธฐํ›„ ์˜ค์ •๋ณด ๊ฐ์ง€ ๋ฐ xAI ๋ถ„์„ ์‹œ์Šคํ…œ\n**Team 3 | ๊ฐœ๋ฐœ์ผ์ž: 2026-08-13 | LoRA Inference Engine ๊ธฐ๋ฐ˜**")
    
    with gr.Tabs():
        with gr.TabItem("๐Ÿ” AI ์˜ค์ •๋ณด ๊ฐ์ง€๊ธฐ (Inference UI)"):
            with gr.Column(elem_classes=["main-container"]):
                input_text = gr.Textbox(label="๋‰ด์Šค ๊ธฐ์‚ฌ ๋˜๋Š” ๊ธฐํ›„ ๊ด€๋ จ ํ…์ŠคํŠธ ์ž…๋ ฅ", placeholder="๋ถ„์„ํ•  ๊ธฐํ›„ ๊ด€๋ จ ๋‰ด์Šค๋‚˜ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”...", lines=4)
                btn_submit = gr.Button("๐Ÿš€ ๊ฒฐ๊ณผ ๋ถ„์„ ์‹คํ–‰ (Run Analysis)", variant="primary")
                gr.Markdown("---")
                
                badge_output = gr.HTML(value='<div style="color: #666;">ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•œ ํ›„ ๋ถ„์„ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํŒ์ • ๊ฒฐ๊ณผ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</div>', label="ํŒ์ •๊ฒฐ๊ณผ")
                
                with gr.Column(elem_classes=["sketch-card"]):
                    gr.HTML('<div class="highlight-title">Highlight Word (xAI ์–ดํ…์…˜ ๋ถ„์„)</div>')
                    highlight_output = gr.HighlightedText(label="์ค‘์š” ๋‹จ์–ด ์–ดํ…์…˜ ๊ฐ€์ค‘์น˜", combine_adjacent=False, show_legend=True)
                
                with gr.Row(elem_classes=["sketch-card"]):
                    gr.Button("๊ฒฐ๊ณผ", variant="secondary", interactive=False)
                    risk_score_output = gr.Textbox(value="์œ„ํ—˜๋„ ์ ์ˆ˜ +0.0007%", label="์œ„ํ—˜๋„ ์ ์ˆ˜", interactive=False)
                
                with gr.Column(elem_classes=["disclaimer-box"]):
                    limitation_output = gr.Markdown("ํ’์ž-๋ฐ˜์–ด๋ฒ• ํ…์ŠคํŠธ ์ •ํ™•๋„ ๋‚ฎ์Œ! โš ๏ธ (Model Card ํ•œ๊ณ„ ์‚ฌ์ „ ๊ณ ์ง€)")
                    
                with gr.Accordion("โš–๏ธ ์ด์˜ ์ œ๊ธฐ ํ†ต๋กœ (Model Card ์œค๋ฆฌ์  ์ฑ…์ž„ ์„ ์–ธ)", open=False):
                    gr.Markdown("๋ชจ๋ธ์˜ ํŒ์ • ๊ฒฐ๊ณผ์— ์˜คํŒ์ด ์žˆ๊ฑฐ๋‚˜ ์ด์˜๊ฐ€ ์žˆ์œผ์‹  ๊ฒฝ์šฐ ์•„๋ž˜ ์„œ์‹์„ ์ œ์ถœํ•ด์ฃผ์„ธ์š”.")
                    appeal_reason = gr.Textbox(label="์ด์˜ ์ œ๊ธฐ ์‚ฌ์œ  ๋ฐ ์†Œ๋ช… ๋‚ด์šฉ")
                    appeal_email = gr.Textbox(label="๊ฐœ๋ฐœ์ž Email")
                    btn_appeal = gr.Button("์ด์˜ ์ œ๊ธฐ ์ œ์ถœ")
                    appeal_status = gr.Textbox(label="์ ‘์ˆ˜ ์ƒํƒœ", interactive=False)
                    
                    btn_appeal.click(fn=file_appeal, inputs=[appeal_reason, appeal_email], outputs=[appeal_status])

            btn_submit.click(fn=analyze_climate_text, inputs=[input_text], outputs=[badge_output, highlight_output, risk_score_output, limitation_output])

        with gr.TabItem("๐Ÿ“‹ Model Card (๋ชจ๋ธ ์นด๋“œ ๋ฐ ์ฑ…์ž„ ์„ ์–ธ)"):
            gr.Markdown("""
                ## ๐Ÿ“„ ์ฑ…์ž„์•ˆ์ „ AI Model Card
                - **๋ชจ๋ธ๋ช…**: Climate Misinfo LoRA Detector
                - **ํŒ€๋ช… ๋ฐ ์ž‘์„ฑ์ผ**: Team 3 | 2026-08-13
                - **์˜๋„๋œ ์‚ฌ์šฉ**: ๊ธฐํ›„ ๋ณ€ํ™” ๊ด€๋ จ ๊ธฐ์‚ฌ ๊ฒ€์ฆ / ๊ธˆ์ง€: ์˜ํ•™ยท๋ฒ•๋ฅ ์  ์ž๋™ ๊ทœ์ œ
                - **ํ•œ๊ณ„ ๋ฐ ์œ„ํ—˜**: ํ’์žยท๋ฐ˜์–ด๋ฒ• ํ—ค๋“œ๋ผ์ธ ๋ฐ ๋งฅ๋ฝ ๋ˆ„๋ฝ ์‹œ ์˜คํŒ ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ
                - **๊ฐœ๋ฐœ์ž ์ฑ…์ž„ ์„ ์–ธ**: ํ•œ๊ณ„๋ฅผ ์‚ฌ์ „ ๊ณ ์ง€ํ•˜๋ฉฐ ์ด์˜ ์ œ๊ธฐ ํ†ต๋กœ๋ฅผ ํ†ตํ•ด ์ˆ˜๋ ด ๋ฐ ๊ฐœ์„ ํ•จ.
            """)

        with gr.TabItem("โš™๏ธ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํ๋ฆ„๋„ (Algorithm Flowchart)"):
            gr.Markdown("1. User -> 2. Preprocessing -> 3. Tokenizing -> 4. LoRA Inference Engine -> 5. xAI Extraction -> 6. Attention Analysis -> 7. Output Generation -> 8. Result & Disclaimer")

demo.launch()