| import os |
| import re |
| import torch |
| import gradio as gr |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification |
| from peft import PeftModel |
|
|
| |
| 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) |
| |
| |
| 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 |
|
|
| |
| 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] |
| |
| 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 |
|
|
| |
| 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)) |
| |
| |
| 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() |