File size: 8,834 Bytes
7803723
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import streamlit as st
import re
from typing import List, Tuple, Dict
from subjectless_predicates_122725_v2 import DOUBT_PREDICATES, SUPPORT_PREDICATES, analyze_objectivity
from korean_sentence_splitter import KoreanSentenceSplitter

def highlight_objectivity(text: str) -> str:
    if not text:
        return ""

    splitter = KoreanSentenceSplitter()
    sentences = splitter.split(text)
    
    highlighted_sentences = []
    
    for sent in sentences:
        matches = []
        
        # Find doubt matches
        for cat, pattern in DOUBT_PREDICATES.items():
            for match in pattern.finditer(sent):
                matches.append((match.start(), match.end(), "doubt", cat))
                
        # Find support matches
        for cat, pattern in SUPPORT_PREDICATES.items():
            for match in pattern.finditer(sent):
                matches.append((match.start(), match.end(), "support", cat))
        
        # Sort matches by start position, then by length (descending) to handle potential overlaps
        matches.sort(key=lambda x: (x[0], -(x[1] - x[0])))
        
        # Filter overlapping matches (keep the longest or first)
        filtered_matches = []
        if matches:
            last_end = -1
            for start, end, mtype, cat in matches:
                if start >= last_end:
                    filtered_matches.append((start, end, mtype, cat))
                    last_end = end
        
        # Build highlighted sentence
        last_idx = 0
        h_sent = ""
        for start, end, mtype, cat in filtered_matches:
            # Add text before match
            h_sent += sent[last_idx:start]
            
            # Add highlighted match
            match_text = sent[start:end]
            if mtype == "doubt":
                color = "#ffcccc" # Light red
                border = "#ff0000"
                h_sent += f'<span style="background-color: {color}; border-bottom: 2px solid {border};" title="{cat}">{match_text}</span>'
            else:
                color = "#ccffcc" # Light green
                border = "#00aa00"
                h_sent += f'<span style="background-color: {color}; border-bottom: 2px solid {border};" title="{cat}">{match_text}</span>'
            
            last_idx = end
        
        h_sent += sent[last_idx:]
        highlighted_sentences.append(h_sent)
        
    return " ".join(highlighted_sentences)

def clear_input():
    st.session_state.main_text_input = ""

def main():
    st.set_page_config(page_title="뉴스 객관성 술어 분석기", layout="wide")
    
    st.title("📰 뉴스 객관성 분석기: 술어 성격 탐지 및 추출")
    
    st.markdown("""
    뉴스 객관성 평가를 위해 술어의 성격을 탐지하고 추출하는 기능을 수행합니다.
    - <span style="background-color: #ffcccc; border-bottom: 2px solid #ff0000;">빨간색 표시</span>: **객관성 의심 (Objectivity Doubt)** - 발언의 주체가 불분명하여 기자의 주관이 개입되었을 가능성이 높은 표현
    - <span style="background-color: #ccffcc; border-bottom: 2px solid #00aa00;">녹색 표시</span>: **객관성 지지 (Objectivity Support)** - 사실 확인이나 구체적인 출처/데이터를 바탕으로 한 객관적 표현
    """, unsafe_allow_html=True)
    
    # 세션 상태 초기화
    if 'main_text_input' not in st.session_state:
        st.session_state.main_text_input = """오늘 주가가 크게 오를 것으로 전망된다. 투자자들 사이에서는 이번 상승세가 당분간 이어질 것이라는 분석이 지배적이다. 반면 일각에서는 거품이라는 지적도 나온다. 실제로 통계청 집계에 따르면 지난달 수출은 역대 최고치를 기록했다. 정부 관계자는 경제 지표가 개선되고 있음이 확인됐다고 밝혔다."""

    col1, col2 = st.columns([1, 1])
    
    with col1:
        st.subheader("입력 텍스트")
        # key를 통해 세션 상태와 직접 연결
        input_text = st.text_area("분석할 기사 내용을 입력하세요.", height=400, 
                                 placeholder="여기에 기사 내용을 붙여넣으세요...",
                                 key="main_text_input")
        
        btn_col1, btn_col2, _ = st.columns([1, 1, 3])
        analyze_clicked = btn_col1.button("분석", type="primary")
        # 초기화 버튼에 콜백 함수 적용
        btn_col2.button("초기화", on_click=clear_input)

    # 분석 버튼 클릭 시 결과 표시
    if analyze_clicked and input_text:
        with col2:
            st.subheader("분석 결과")
            with st.spinner("분석 중..."):
                highlighted_html = highlight_objectivity(input_text)
                st.markdown(f'<div style="line-height: 1.8; font-size: 1.1em; border: 1px solid #ddd; padding: 20px; border-radius: 5px; background-color: white;">{highlighted_html}</div>', unsafe_allow_html=True)
                
                # 통계 요약
                stats = analyze_objectivity(input_text)
                
                st.write("---")
                s_col1, s_col2, s_col3 = st.columns(3)
                s_col1.metric("의심 문장", stats["doubt_count"])
                s_col2.metric("지지 문장", stats["support_count"])
                if stats["objectivity_ratio"] is not None:
                    st.progress(stats["objectivity_ratio"])
                    st.write(f"**객관성 지표:** {stats['objectivity_ratio']:.2%}")
                
                with st.expander("검출된 술어 상세 목록"):
                    tab1, tab2 = st.tabs(["의심 술어 (DOUBT)", "지지 술어 (SUPPORT)"])
                    with tab1:
                        if stats["doubt_predicates"]:
                            st.write(", ".join(set(stats["doubt_predicates"])))
                        else:
                            st.write("발견되지 않음")
                    with tab2:
                        if stats["support_predicates"]:
                            st.write(", ".join(set(stats["support_predicates"])))
                        else:
                            st.write("발견되지 않음")

    st.write("---")
    st.subheader("💡 설명서: 술어 분류 체계")
    st.markdown("`subjectless_predicates_122725_v2.py` 참조")
    
    desc_col1, desc_col2 = st.columns(2)
    
    with desc_col1:
        st.markdown("### 🔴 객관성 의심 술어 (DOUBT): 무주체 피동형")
        st.markdown("""
| 대분류 | 설명 | 주요 예시 |
| :--- | :--- | :--- |
| **분석/해석형** | 사건의 의미를 주관적으로 풀이 | 분석된다, 해석된다, ~라는 분석이다 |
| **전망/예측형** | 불확실한 미래를 단정적으로 추측 | 전망된다, 예상된다, 점쳐진다 |
| **관측/추정형** | 뚜렷한 근거 없이 미루어 짐작 | 관측된다, 추정된다, 추측된다 |
| **전언/보도형** | 출처를 흐리며 말을 전달 | 알려졌다, 전해졌다, ~라는 소식이다 |
| **평가/판단형** | 가치 판단이 개입된 서술 | 평가된다, 여겨진다, ~라는 판단이다 |
| **비판/지적형** | 특정 입장에서 부정적으로 언급 | 비판받는다, 지적된다, 논란이 일고 있다 |
| **제기/거론형** | 화제를 수면 위로 올리는 서술 | 제기된다, 거론된다, 언급된다 |
| **우려/의혹형** | 부정적 가능성을 강조 | 우려가 나온다, 의혹이 제기됐다 |
| **가능성형** | 여지를 두는 표현 | 가능성이 크다, 배제할 수 없다 |
| **분위기형** | 주변 상황을 추상적으로 묘사 | 분위기다, 목소리가 높다, 기류가 감지된다 |
| **주장/입장형** | 특정인의 말을 무주체로 전달 | 주장된다, ~라는 입장이다 |
| **시각/견해형** | 관점을 제시 | 시각이다, 견해가 지배적이다 |
| **격찬/혹평형** | 극단적인 감정적 평가 | 찬사가 쏟아졌다, 혹평을 받았다 |
| **관용표현형** | 객관성을 흐리는 상투적 표현 | ~인 셈이다, ~로 보인다, 아닌가 싶다 |
| **완화표현형** | 단정을 피하려는 회피성 표현 | ~듯 보인다, ~일 것 같다 |
        """)

    with desc_col2:
        st.markdown("### 🟢 객관성 지지 술어 (SUPPORT)")
        st.markdown("""
| 대분류 | 설명 | 주요 예시 |
| :--- | :--- | :--- |
| **확인/검증형** | 사실 관계가 분명히 밝혀짐 | 확인됐다, 밝혀졌다, 드러났다, 입증됐다 |
| **발견/탐지형** | 구체적인 실체를 찾아냄 | 발견됐다, 적발됐다, 파악됐다 |
| **기록/집계형** | 수치나 데이터에 기반한 서술 | 기록됐다, 집계됐다, 나타났다, 조사됐다 |
        """)

if __name__ == "__main__":
    main()