# 정치 편향 방지 · 사실 기반 중립 재구성 (BERT 분류기 버전) # - 분류: bucketresearch/politicalBiasBERT (left/center/right) # - 재구성: OpenAI로 사실 중심 요약/재작성 # - 옵션: 네이버 뉴스 검색 import os from typing import List, Dict, Tuple import streamlit as st import requests import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline # ========================= # Config # ========================= APP_TITLE = "정치 편향 분석(BERT) · 사실 기반 중립 재구성!" OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # 선택 NAVER_ID = os.getenv("NAVER_ID") # 선택 NAVER_SECRET = os.getenv("NAVER_SECRET") MODEL_ID = "bucketresearch/politicalBiasBERT" LABELS = ["left", "center", "right"] # 모델 카드 정의 st.set_page_config(page_title=APP_TITLE, page_icon="🧭", layout="wide") st.title(APP_TITLE) st.caption("PoliticalBiasBERT로 편향(좌/중/우) 분류 → 화면에는 '사실 기반 중립 재구성'만 노출") # ========================= # Model (cached) # ========================= @st.cache_resource(show_spinner=True) def load_bias_pipeline(): tok = AutoTokenizer.from_pretrained(MODEL_ID) mdl = AutoModelForSequenceClassification.from_pretrained(MODEL_ID) device = 0 if torch.cuda.is_available() else -1 clf = pipeline( "text-classification", model=mdl, tokenizer=tok, return_all_scores=True, device=device ) return clf def classify_bias(clf, text: str) -> Tuple[str, List[float]]: scores = clf(text)[0] # list of dicts: [{"label":"LABEL_0","score":...}, ...] # 모델이 LABEL_0/1/2를 쓰므로 index 기준으로 정렬되어 들어온다는 가정 probs = [s["score"] for s in scores] # [left, center, right] 순 pred_idx = int(torch.tensor(probs).argmax().item()) return LABELS[pred_idx], probs # ========================= # Naver News (optional) # ========================= def fetch_naver_news(query: str, display: int = 10) -> List[Dict[str,str]]: if not (NAVER_ID and NAVER_SECRET): return [] url = "https://openapi.naver.com/v1/search/news.json" headers = {"X-Naver-Client-Id": NAVER_ID, "X-Naver-Client-Secret": NAVER_SECRET} params = {"query": query, "display": min(display, 30), "start": 1, "sort": "date"} try: r = requests.get(url, headers=headers, params=params, timeout=15) if r.status_code != 200: return [] items = r.json().get("items", []) return [{ "title": it.get("title",""), "desc": it.get("description",""), "link": it.get("link","") } for it in items] except Exception: return [] # ========================= # OpenAI: Fact-based neutral rewrite (optional) # ========================= def generate_fact_based(text: str) -> str: if not OPENAI_API_KEY: return "(OPENAI_API_KEY 미설정: 재구성 생략됨)" import openai openai.api_key = OPENAI_API_KEY prompt = ( "다음 텍스트를 정치적 해석/의견 없이, 사실 중심의 중립 기사로 재구성하세요.\n" "규칙: 1) 누가·언제·어디서·무엇을 중심 2) 평가/추측 삭제 3) 수치/날짜는 원문 범위 4) 한국어 5~7문장\n\n" f"[원문]\n{text}\n\n[중립 기사]" ) try: resp = openai.ChatCompletion.create( model="gpt-4o-mini", messages=[{"role":"user","content":prompt}], temperature=0.3, max_tokens=420, ) return resp["choices"][0]["message"]["content"].strip() except Exception as e: return f"(재구성 실패: {e})" # ========================= # Sidebar # ========================= with st.sidebar: st.subheader("모델 상태") with st.spinner("BERT 모델 로딩 중… 처음 한 번만 기다리면 됨"): clf = load_bias_pipeline() st.success("PoliticalBiasBERT 로드 완료") st.caption("좌/중/우 분류는 내부 진단용으로만 사용. 화면은 사실 기반 재구성 위주.") # ========================= # Main # ========================= st.markdown("### 1) (선택) 네이버 뉴스 검색") q = st.text_input("검색어", value="미 대선") cnt = st.slider("표시 개수", 1, 20, 10) news_items: List[Dict[str,str]] = [] if st.button("뉴스 불러오기"): with st.spinner("네이버 뉴스 수집 중…"): news_items = fetch_naver_news(q, cnt) if not news_items: st.info("네이버 API 키가 없거나 호출 실패. 아래 자유 입력으로 테스트하세요.") st.markdown("### 2) 텍스트 분석 & 사실 기반 중립 재구성") c1, c2 = st.columns(2) with c1: sample = f"{news_items[0]['title']} — {news_items[0]['desc']}" if news_items else "" text = st.text_area("분석할 텍스트(뉴스 제목+요약 등)", value=sample, height=220) with c2: if st.button("분석 및 중립 재구성 실행"): if not text.strip(): st.warning("텍스트를 입력하세요.") else: # 내부 분류(진단용) pred, probs = classify_bias(clf, text) # 화면 노출: 사실 기반 재구성 st.markdown("#### ✅ 사실 기반 중립 재구성 결과") article = generate_fact_based(text) st.write(article) # 진단/출처 with st.expander("진단 보기(내부 편향 확률)"): st.write(f"예측: **{pred}**") st.write(f"확률 [left, center, right]: {probs}") if news_items: with st.expander("원문 링크"): st.write(news_items[0].get("link","(링크 없음)")) st.markdown("---") st.caption("데모 용도. 실제 서비스는 출처 추출·사실 검증·정책 필터링을 추가해야 함.")