Spaces:
Sleeping
Sleeping
| 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() | |