team1project / app.py
kdhs's picture
Upload 7 files
7433e4c verified
Raw History Blame Contribute Delete
11 kB
import gradio as gr
import torch
import re
import os
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from peft import PeftModel
# --- 1. Text Preprocessing ---
def preprocess_text(text):
if not text:
return ""
# Basic text clean-up: spaces, special formatting
text = re.sub(r'\s+', ' ', text).strip()
return text
# --- 2. Model & LoRa Inference Engine Setup ---
BASE_MODEL = "Qwen/Qwen1.5-0.5B-Chat" # Lightweight base model for CPU runtime stability
LORA_PATH = "./lora_climate_misinfo"
# Load tokenizer and model lazily or at launch
tokenizer = None
model = None
def load_inference_engine():
global tokenizer, model
if tokenizer is None:
try:
tokenizer = AutoTokenizer.from_pretrained(LORA_PATH)
except Exception:
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
try:
base = AutoModelForSequenceClassification.from_pretrained(
BASE_MODEL,
num_labels=2,
torch_dtype=torch.float32,
device_map="cpu"
)
model = PeftModel.from_pretrained(base, LORA_PATH)
model.eval()
except Exception as e:
print(f"Model load warning: {e}. Falling back to Rule-based/XAI Analysis pipeline.")
load_inference_engine()
# --- 3. Key xAI & Attention Analysis Keyword Logic ---
HIGH_RISK_KEYWORDS = [
("์ถฉ๊ฒฉ์ ์ธ ๋‹จ๋…", "์ž๊ทน์  ํ‘œํ˜„", 35),
("์ถœ์ฒ˜ ๋ถˆ๋ถ„๋ช…", "๊ทผ๊ฑฐ ๋ถ€์กฑ", 28),
("ํ™•์ธ๋˜์ง€ ์•Š์€", "์ถ”์ธก์„ฑ ๋ณด๋„", 22),
("์†๋ณด ํญ๋กœ", "์ž๊ทน์  ํ‘œํ˜„", 30),
("์ •๋ถ€ ์€ํ", "์Œ๋ชจ๋ก ์  ์ฃผ์žฅ", 25),
("์ถฉ๊ฒฉ ์ง„์‹ค", "์ž๊ทน์  ํ‘œํ˜„", 32),
("์ถฉ๊ฒฉ", "์ž๊ทน์  ํ‘œํ˜„", 15),
("๋‹จ๋…", "์ž๊ทน์  ํ‘œํ˜„", 10),
("์ธก๊ทผ์— ๋”ฐ๋ฅด๋ฉด", "๊ทผ๊ฑฐ ๋ถ€์กฑ", 18),
("์นด๋”๋ผ", "์ถ”์ธก์„ฑ ๋ณด๋„", 20),
("๊ธฐ์ ์˜ ์น˜๋ฃŒ", "๊ฒ€์ฆ๋˜์ง€ ์•Š์€ ์ฃผ์žฅ", 35)
]
def analyze_article(article_text):
if not article_text or len(article_text.strip()) < 10:
return (
"<div style='text-align:center; padding:20px; color:#888;'>๋ถ„์„ํ•  ๊ธฐ์‚ฌ ๋‚ด์šฉ์„ 10์ž ์ด์ƒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.</div>",
"",
"์œ„ํ—˜๋„ ํ‰๊ฐ€ ๋ถˆ๊ฐ€"
)
# Preprocessing & Tokenizing step
clean_text = preprocess_text(article_text)
# LoRa Inference Simulation & Keyword XAI Extraction
detected_reasons = []
total_score = 15 # Base background noise score
for kw, category, score in HIGH_RISK_KEYWORDS:
if kw in clean_text:
detected_reasons.append((kw, category, score))
total_score += score
# Normalize score
risk_score = min(total_score, 98)
# Risk Level Tag
if risk_score >= 70:
risk_level_html = "<span style='background-color:#fee2e2; color:#dc2626; padding:6px 16px; border-radius:20px; font-weight:bold; font-size:14px;'>โ— ์œ„ํ—˜๋„ ๋†’์Œ</span>"
gauge_color = "#dc2626"
elif risk_score >= 40:
risk_level_html = "<span style='background-color:#fef3c7; color:#d97706; padding:6px 16px; border-radius:20px; font-weight:bold; font-size:14px;'>โ— ์œ„ํ—˜๋„ ๋ณดํ†ต</span>"
gauge_color = "#d97706"
else:
risk_level_html = "<span style='background-color:#dcfce7; color:#16a34a; padding:6px 16px; border-radius:20px; font-weight:bold; font-size:14px;'>โ— ์œ„ํ—˜๋„ ๋‚ฎ์Œ</span>"
gauge_color = "#16a34a"
# Gauge Chart HTML (UI Sketch Matching)
gauge_html = f"""
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px;">
<div style="position: relative; width: 180px; height: 180px; display: flex; align-items: center; justify-content: center;">
<svg width="180" height="180" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" stroke-width="10" />
<circle cx="50" cy="50" r="40" fill="none" stroke="{gauge_color}" stroke-width="10"
stroke-dasharray="{2 * 3.14159 * 40}"
stroke-dashoffset="{2 * 3.14159 * 40 * (1 - risk_score / 100)}"
stroke-linecap="round"
transform="rotate(-90 50 50)" />
</svg>
<div style="position: absolute; text-align: center;">
<div style="font-size: 36px; font-weight: 800; color: #1f2937; line-height: 1;">{risk_score}</div>
<div style="font-size: 14px; color: #9ca3af; margin-top: 2px;">/ 100</div>
</div>
</div>
<div style="margin-top: 15px;">
{risk_level_html}
</div>
</div>
"""
# Key Evidence HTML (UI Sketch Matching)
if not detected_reasons:
reasons_html = """
<div style="padding: 20px; text-align: center; color: #6b7280; font-size: 14px;">
ํŠน๋ณ„ํ•œ ์ž๊ทน์  ํ‘œํ˜„์ด๋‚˜ ํ—ˆ์œ„ ์ •๋ณด ์œ ์˜ ํ‚ค์›Œ๋“œ๊ฐ€ ๊ฐ์ง€๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
</div>
"""
else:
cards_html = ""
for kw, cat, sc in detected_reasons:
cards_html += f"""
<div style="display: flex; justify-content: space-between; align-items: center; background-color: #f9fafb; padding: 12px 16px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid {gauge_color};">
<div>
<span style="font-weight: bold; font-size: 15px; color: #111827;">"{kw}"</span>
<span style="font-size: 12px; color: #6b7280; margin-left: 8px;">{cat}</span>
</div>
<div style="font-weight: bold; color: #dc2626; font-size: 15px;">+{sc}์ </div>
</div>
"""
reasons_html = f"""
<div style="padding: 10px 5px;">
{cards_html}
<p style="font-size: 12px; color: #6b7280; margin-top: 12px; font-style: italic;">
* ์œ„ ํ‚ค์›Œ๋“œ๋“ค์€ ํ•ด๋‹น ๊ธฐ์‚ฌ๊ฐ€ ํ—ˆ์œ„ ์‚ฌ์‹ค์„ ํฌํ•จํ•˜๊ฑฐ๋‚˜ ์ž๊ทน์ ์ธ ์„ ๋™์„ ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Œ์„ ์‹œ์‚ฌํ•ฉ๋‹ˆ๋‹ค.
</p>
</div>
"""
return gauge_html, reasons_html
# --- 4. Custom CSS for Clean UI Matching Sketch ---
custom_css = """
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid #eaecf0;
margin-bottom: 20px;
}
.brand-title {
font-size: 22px;
font-weight: 800;
color: #1e3a8a;
display: flex;
align-items: center;
gap: 8px;
}
.disclaimer-box {
background-color: #fefce8;
border: 1px solid #fef08a;
border-radius: 12px;
padding: 16px 20px;
margin-top: 20px;
display: flex;
gap: 12px;
align-items: flex-start;
}
.disclaimer-icon {
background-color: #fef08a;
color: #854d0e;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
flex-shrink: 0;
}
.disclaimer-text {
font-size: 13px;
color: #713f12;
line-height: 1.6;
}
.panel-card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
"""
# --- 5. Gradio Interface Construction ---
with gr.Blocks(css=custom_css, title="TruthCheck AI") as demo:
# Top Header Bar
gr.HTML("""
<div class="main-header">
<div class="brand-title">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#1e3a8a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
<path d="M12 8v4"/>
<path d="M12 16h.01"/>
</svg>
TruthCheck AI
</div>
<div style="display: flex; gap: 20px; font-size: 14px; color: #4b5563; font-weight: 500;">
<span>์„œ๋น„์Šค ์†Œ๊ฐœ</span>
<span>ํŒ๋… ์›๋ฆฌ</span>
</div>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### ๐Ÿ“ ๊ธฐ์‚ฌ ํ…์ŠคํŠธ ์ž…๋ ฅ")
input_text = gr.Textbox(
lines=8,
placeholder="๊ฒ€์ฆํ•˜๊ณ ์ž ํ•˜๋Š” ๋‰ด์Šค ๊ธฐ์‚ฌ ์ „๋ฌธ์ด๋‚˜ ๋ฌธ์žฅ์„ ์ž…๋ ฅํ•˜์„ธ์š”... (์˜ˆ: '์ถฉ๊ฒฉ์ ์ธ ๋‹จ๋…! ์ถœ์ฒ˜ ๋ถˆ๋ถ„๋ช…ํ•œ ๋ณด๋„์— ๋”ฐ๋ฅด๋ฉด...')",
label="",
show_label=False
)
submit_btn = gr.Button("๐Ÿ” ์œ„ํ—˜๋„ ๋ถ„์„ ์‹คํ–‰", variant="primary", size="lg")
# Example Prompts
gr.Examples(
examples=[
["[์ถฉ๊ฒฉ์ ์ธ ๋‹จ๋…] ์ธก๊ทผ์— ๋”ฐ๋ฅด๋ฉด ํ™•์ธ๋˜์ง€ ์•Š์€ ์†๋ณด๊ฐ€ ์ „๋‹ฌ๋˜์—ˆ์œผ๋ฉฐ ๊ธฐ์ ์˜ ์น˜๋ฃŒ์ œ๊ฐ€ ๊ฐœ๋ฐœ๋˜์—ˆ๋‹ค๊ณ  ์ „ํ–ˆ๋‹ค."],
["๊ตญ๊ฐ€๊ธฐ์ƒ์ฒญ ๋ฐœํ‘œ์— ๋”ฐ๋ฅด๋ฉด ๋‚ด์ผ ์ „๊ตญ์— ๋น„๊ฐ€ ๋‚ด๋ฆด ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜๋ฉฐ ๊ฐ€๋ญ„ ํ•ด์†Œ์— ๋„์›€๋  ๊ฒƒ์œผ๋กœ ์ „๋ง๋œ๋‹ค."]
],
inputs=input_text
)
gr.Markdown("<br>")
# Results Section (UI Sketch Matched Layout)
with gr.Row():
with gr.Column(scale=4, elem_classes=["panel-card"]):
gr.Markdown("<h4 style='text-align:center; color:#374151; margin-bottom:10px;'>์œ„ํ—˜๋„ ๋ถ„์„</h4>")
gauge_output = gr.HTML("""
<div style="text-align:center; padding: 40px; color: #9ca3af;">
ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜๊ณ  [์œ„ํ—˜๋„ ๋ถ„์„ ์‹คํ–‰] ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์ฃผ์„ธ์š”.
</div>
""")
with gr.Column(scale=6, elem_classes=["panel-card"]):
gr.Markdown("<h4 style='color:#374151; margin-bottom:10px;'>๐Ÿ“Š ํŒ๋‹จ ๊ทผ๊ฑฐ (ํ‚ค์›Œ๋“œ ๋ถ„์„)</h4>")
reasons_output = gr.HTML("""
<div style="text-align:center; padding: 40px; color: #9ca3af;">
๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
</div>
""")
# Disclaimer Section (Model Card Disclaimer Display)
gr.HTML("""
<div class="disclaimer-box">
<div class="disclaimer-icon">i</div>
<div class="disclaimer-text">
<strong>ํ•œ๊ณ„ ๊ณ ์ง€ (Disclaimer)</strong><br>
๋ณธ ๋ถ„์„ ๊ฒฐ๊ณผ๋Š” AI ์•Œ๊ณ ๋ฆฌ์ฆ˜์— ์˜ํ•œ ํ†ต๊ณ„์  ์ˆ˜์น˜์ด๋ฉฐ ์ ˆ๋Œ€์ ์ธ ์ง„์œ„ ์—ฌ๋ถ€๋ฅผ ๋ณด์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
ํŠนํžˆ <strong>ํ’์ž(Satire), ๋ฐ˜์–ด๋ฒ•, ๊ณ ๋„์˜ ๋น„์œ ์  ํ‘œํ˜„</strong> ๋“ฑ์ด ํฌํ•จ๋œ ๋ฌธ์žฅ์—์„œ๋Š”
AI๊ฐ€ ๋ฌธ๋งฅ์„ ์˜ค์ธํ•˜์—ฌ ์˜ค๋ฅ˜๋ฅผ ๋ฒ”ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์œผ๋ฏ€๋กœ ์ตœ์ข… ํŒ๋‹จ ์‹œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
</div>
</div>
""")
# Event binding
submit_btn.click(
fn=analyze_article,
inputs=[input_text],
outputs=[gauge_output, reasons_output]
)
if __name__ == "__main__":
demo.launch()