File size: 19,115 Bytes
a9b0b3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import streamlit as st
import PyPDF2
import docx
import time
import pandas as pd
from io import BytesIO
from rouge_score import rouge_scorer # <-- [MỚI] Thư viện tính điểm học thuật
import plotly.express as px
import os
import smtplib
from email.message import EmailMessage

# --- IMPORT CÁC MODULE XỬ LÝ ---
from summarizer_ai import TextSummarizer
from textrank_summarizer import TextRankSummarizer
from text_cleaner import TextPreprocessor
from groq_summarizer import GroqSummarizer
from cohere_summarizer import CohereSummarizer
import database 
import api_keys 

st.set_page_config(page_title="AI Summarizer Pro", page_icon="📝", layout="wide")
database.init_db()

@st.cache_resource
def load_models():
    return (
        TextSummarizer(), TextRankSummarizer(), TextPreprocessor(), 
        GroqSummarizer(api_keys.GROQ_KEY), CohereSummarizer(api_keys.COHERE_KEY)
    )

def _ensure_auth_state():
    if "user" not in st.session_state:
        st.session_state.user = None

def _mask_email(email: str) -> str:
    email = (email or "").strip()
    if "@" not in email:
        return "***"
    name, domain = email.split("@", 1)
    if len(name) <= 2:
        name_masked = name[:1] + "*"
    else:
        name_masked = name[:2] + "*" * (len(name) - 2)
    return f"{name_masked}@{domain}"

def _send_reset_email(to_email: str, code: str):
    smtp_user = os.getenv("SMTP_USER", "").strip() or str(st.secrets.get("SMTP_USER", "")).strip()
    smtp_app_password = os.getenv("SMTP_APP_PASSWORD", "").strip() or str(st.secrets.get("SMTP_APP_PASSWORD", "")).strip()
    if not smtp_user or not smtp_app_password:
        return False, "Chưa cấu hình SMTP. Hãy set SMTP_USER/SMTP_APP_PASSWORD (env hoặc .streamlit/secrets.toml)."

    msg = EmailMessage()
    msg["Subject"] = "AI Summarizer Pro - Ma dat lai mat khau"
    msg["From"] = smtp_user
    msg["To"] = to_email
    msg.set_content(
        "Ban da yeu cau dat lai mat khau.\n\n"
        f"Ma xac nhan (OTP): {code}\n"
        "Ma co hieu luc 10 phut. Neu khong phai ban, hay bo qua email nay.\n"
    )

    try:
        with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
            server.login(smtp_user, smtp_app_password)
            server.send_message(msg)
        return True, "Da gui ma OTP qua email."
    except Exception as e:
        return False, f"Gui email that bai: {e}"

def _render_auth_sidebar():
    st.sidebar.header("👤 Tài khoản")

    if st.session_state.user:
        st.sidebar.success(f"Xin chào, {st.session_state.user['username']}")
        if st.sidebar.button("Đăng xuất"):
            st.session_state.user = None
            st.rerun()
        return True

    tab_login, tab_register, tab_forgot = st.sidebar.tabs(["Đăng nhập", "Đăng ký", "Quên mật khẩu"])

    with tab_login:
        with st.form("login_form", clear_on_submit=False):
            username = st.text_input("Username", placeholder="vd: thinh")
            password = st.text_input("Password", type="password")
            submitted = st.form_submit_button("Đăng nhập", type="primary")
        if submitted:
            ok, user, msg = database.authenticate_user(username, password)
            if ok:
                st.session_state.user = user
                st.sidebar.success(msg)
                st.rerun()
            else:
                st.sidebar.error(msg)

    with tab_register:
        with st.form("register_form", clear_on_submit=True):
            username = st.text_input("Username (bắt buộc)")
            email = st.text_input("Email (tuỳ chọn)", placeholder="name@example.com")
            password = st.text_input("Password (bắt buộc)", type="password")
            confirm = st.text_input("Nhập lại password", type="password")
            submitted = st.form_submit_button("Tạo tài khoản", type="primary")
        if submitted:
            if password != confirm:
                st.sidebar.error("Password nhập lại không khớp.")
            elif len((password or "")) < 6:
                st.sidebar.error("Password tối thiểu 6 ký tự.")
            else:
                ok, msg = database.create_user(username=username, password=password, email=email)
                if ok:
                    st.sidebar.success(msg)
                else:
                    st.sidebar.error(msg)

    with tab_forgot:
        st.caption("Nhập username hoặc email đã đăng ký để nhận mã OTP.")
        smtp_user_present = bool(os.getenv("SMTP_USER", "").strip() or str(st.secrets.get("SMTP_USER", "")).strip())
        smtp_pass_present = bool(os.getenv("SMTP_APP_PASSWORD", "").strip() or str(st.secrets.get("SMTP_APP_PASSWORD", "")).strip())
        if not (smtp_user_present and smtp_pass_present):
            st.warning("SMTP chưa được cấu hình cho phiên chạy hiện tại.")

        with st.form("forgot_request_form", clear_on_submit=True):
            identifier = st.text_input("Username hoặc Email")
            submitted = st.form_submit_button("Gửi mã OTP", type="primary")

        if submitted:
            ok, email, code_or_msg = database.create_password_reset_code(identifier)
            if ok:
                send_ok, send_msg = _send_reset_email(email, code_or_msg)
                if send_ok:
                    st.sidebar.success(f"{send_msg} ({_mask_email(email)})")
                else:
                    st.sidebar.error(send_msg)
            else:
                st.sidebar.info(code_or_msg)

        st.divider()
        st.caption("Sau khi nhận OTP, nhập mã và mật khẩu mới.")

        with st.form("forgot_reset_form", clear_on_submit=True):
            identifier2 = st.text_input("Username hoặc Email (để đặt lại)")
            code = st.text_input("Mã OTP (6 số)")
            new_password = st.text_input("Mật khẩu mới", type="password")
            confirm = st.text_input("Nhập lại mật khẩu mới", type="password")
            submitted2 = st.form_submit_button("Đổi mật khẩu", type="primary")

        if submitted2:
            if new_password != confirm:
                st.sidebar.error("Password nhập lại không khớp.")
            else:
                ok2, msg2 = database.reset_password_with_code(identifier2, code, new_password)
                if ok2:
                    st.sidebar.success(msg2)
                else:
                    st.sidebar.error(msg2)

    return False

# ==========================================
# HÀM TÍNH TOÁN CÁC ĐỘ ĐO (METRICS)
# ==========================================
def calc_novelty(original_text, summary_text):
    """Tính tỷ lệ phần trăm từ vựng mới được AI tạo ra (Độ sáng tạo)"""
    orig_set = set(original_text.lower().split())
    summ_set = set(summary_text.lower().split())
    if not summ_set: return 0.0
    new_words = summ_set - orig_set
    return round((len(new_words) / len(summ_set)) * 100, 1)

def calc_rouge_l(reference_text, summary_text):
    """Tính điểm ROUGE-L (Mức độ hành văn giống với con người)"""
    if not reference_text.strip(): return 0.0
    scorer = rouge_scorer.RougeScorer(['rougeL'], use_stemmer=False)
    scores = scorer.score(reference_text, summary_text)
    return round(scores['rougeL'].fmeasure * 100, 1)

def extract_text_from_file(uploaded_file):
    try:
        filename = uploaded_file.name
        if filename.endswith('.txt'): return uploaded_file.getvalue().decode("utf-8")
        elif filename.endswith('.pdf'):
            pdf_reader = PyPDF2.PdfReader(BytesIO(uploaded_file.read()))
            return "".join([page.extract_text() + "\n" for page in pdf_reader.pages if page.extract_text()])
        elif filename.endswith('.docx'):
            doc = docx.Document(BytesIO(uploaded_file.read()))
            return "\n".join([para.text for para in doc.paragraphs])
    except Exception as e:
        st.error(f"Lỗi đọc file: {e}")
    return ""

# ==========================================
# GIAO DIỆN CHÍNH
# ==========================================
_ensure_auth_state()
is_authed = _render_auth_sidebar()

st.title("📝 Hệ thống Tóm tắt & Nghiên cứu Đánh giá AI")
st.markdown("Đồ án chuyên sâu: Phân tích hiệu năng, đo lường độ sáng tạo (Novelty) và điểm chuẩn ROUGE giữa các thuật toán.")

if not is_authed:
    st.info("Vui lòng đăng nhập hoặc đăng ký ở sidebar để sử dụng hệ thống.")
    st.stop()

(ai_summarizer, textrank_summarizer, text_cleaner, groq_summarizer, cohere_summarizer) = load_models()

st.sidebar.header("⚙️ Cấu hình chung")
summary_length = st.sidebar.slider("Độ dài tóm tắt mong muốn (số từ):", 30, 1000, 100)

st.subheader("📥 Dữ liệu đầu vào")
uploaded_file = st.file_uploader("📂 Tải lên tài liệu (PDF, DOCX, TXT)", type=["pdf", "docx", "txt"])
input_content = extract_text_from_file(uploaded_file) if uploaded_file else ""

c_input, c_ref = st.columns(2)
with c_input:
    input_text = st.text_area("Nội dung văn bản cần xử lý (Bắt buộc):", value=input_content, height=200)
with c_ref:
    reference_text = st.text_area("Bản tóm tắt chuẩn của con người (Tùy chọn - Dùng để tính điểm ROUGE):", height=200, placeholder="Nhập bản tóm tắt mẫu vào đây để AI so sánh độ chính xác...")

cleaned_text = text_cleaner.clean_text(input_text)
original_word_count = len(cleaned_text.split())

tab1, tab2, tab3 = st.tabs(["📝 Tóm tắt Đơn", "⚖️ So sánh Đa mô hình", "📊 Dashboard & Lịch sử DB"])

# ---------------------------------------------------------
# TAB 1: TÓM TẮT ĐƠN
# ---------------------------------------------------------
with tab1:
    method = st.selectbox("Chọn mô hình AI:", [
        "Thông minh (AI T5 - Viết lại câu)", "Trích xuất ý chính (TextRank)",
        "⚡ Siêu tốc độ (Groq Llama 3 API)", "🌟 Tóm tắt chuyên sâu (Cohere API)"
    ])
    
    if st.button("🚀 Chạy Mô hình Đơn", type="primary"):
        if original_word_count < 20: st.warning("⚠️ Văn bản quá ngắn.")
        else:
            with st.spinner(f"🤖 Đang xử lý bằng {method}..."):
                start_time = time.time()
                try:
                    if "T5" in method: result = ai_summarizer.summarize(cleaned_text, max_len=summary_length)
                    elif "Groq" in method: result = groq_summarizer.summarize(cleaned_text, max_words=summary_length)
                    elif "Cohere" in method: result = cohere_summarizer.summarize(cleaned_text, max_words=summary_length)
                    else: result = textrank_summarizer.summarize(cleaned_text, num_sentences=max(1, summary_length // 20))
                    
                    p_time = round(time.time() - start_time, 2)
                    sum_count = len(result.split())
                    novelty = calc_novelty(cleaned_text, result)
                    rouge = calc_rouge_l(reference_text, result)
                    
                    st.success(result)
                    if not result.startswith("⚠️"):
                        database.save_summary(method, original_word_count, sum_count, p_time, cleaned_text, result, novelty, rouge)
                    
                    m1, m2, m3, m4 = st.columns(4)
                    m1.metric("⏱️ Thời gian", f"{p_time}s")
                    m2.metric("📉 Tỷ lệ nén", f"{round((sum_count/original_word_count)*100, 1)}%")
                    m3.metric("🧠 Độ sáng tạo (Novelty)", f"{novelty}%")
                    m4.metric("🎯 Điểm ROUGE-L", f"{rouge}%" if reference_text else "N/A")
                except Exception as e:
                    st.error(f"Lỗi: {e}")

# ---------------------------------------------------------
# TAB 2: SO SÁNH ĐA MÔ HÌNH
# ---------------------------------------------------------
with tab2:
    st.info("Chế độ này sẽ gửi văn bản đến 4 AI cùng lúc. Kèm theo chấm điểm Novelty (Tỷ lệ sinh từ mới) và ROUGE-L.")
    if st.button("⚖️ Bắt đầu Đại chiến AI (Chạy tất cả)", type="primary"):
        if original_word_count < 20: st.warning("⚠️ Văn bản quá ngắn.")
        else:
            col1, col2 = st.columns(2)
            col3, col4 = st.columns(2)
            
            def render_result(col, title, res, time_taken, method_name):
                with col:
                    st.markdown(f"### {title}")
                    st.write(res)
                    if not res.startswith("⚠️"):
                        sum_cnt = len(res.split())
                        nov = calc_novelty(cleaned_text, res)
                        rg = calc_rouge_l(reference_text, res)
                        st.caption(f"⏱️ {time_taken}s | 📝 {sum_cnt} từ | 🧠 Novelty: {nov}% | 🎯 ROUGE: {rg if reference_text else 'N/A'}")
                        database.save_summary(method_name, original_word_count, sum_cnt, time_taken, cleaned_text, res, nov, rg)

            # 1. Llama 3 (Groq)
            start_t = time.time()
            res_groq = groq_summarizer.summarize(cleaned_text, max_words=summary_length)
            render_result(col1, "⚡ Groq (Llama 3)", res_groq, round(time.time() - start_t, 2), "⚡ Siêu tốc độ (Groq Llama 3 API)")
            
            # 2. Cohere
            start_t = time.time()
            res_co = cohere_summarizer.summarize(cleaned_text, max_words=summary_length)
            render_result(col2, "🌟 Cohere API", res_co, round(time.time() - start_t, 2), "🌟 Tóm tắt chuyên sâu (Cohere API)")
            
            # 3. T5 Local
            start_t = time.time()
            res_t5 = ai_summarizer.summarize(cleaned_text, max_len=summary_length)
            render_result(col3, "🧠 AI T5 (Offline)", res_t5, round(time.time() - start_t, 2), "Thông minh (AI T5 - Viết lại câu)")
                
            # 4. TextRank
            start_t = time.time()
            res_tr = textrank_summarizer.summarize(cleaned_text, num_sentences=max(1, summary_length // 20))
            render_result(col4, "✂️ TextRank", res_tr, round(time.time() - start_t, 2), "Trích xuất ý chính (TextRank)")

# ---------------------------------------------------------
# TAB 3: THỐNG KÊ & BIỂU ĐỒ
# ---------------------------------------------------------
with tab3:
    history_data = database.get_history()
    if len(history_data) == 0:
        st.write("Chưa có dữ liệu. Hãy chạy tóm tắt vài lần để xem biểu đồ!")
    else:
        df = pd.DataFrame(history_data, columns=["ID", "Thời gian", "Phương pháp", "Từ (Gốc)", "Từ (Tóm tắt)", "Thời gian xử lý (s)", "Văn bản gốc", "Kết quả", "Novelty (%)", "ROUGE-L (%)"])
        
        def shorten_name(name):
            if "T5" in name: return "AI T5 (Local)"
            if "TextRank" in name: return "TextRank"
            if "Groq" in name: return "Groq Llama 3"
            if "Cohere" in name: return "Cohere"
            return name
            
        df["Tên rút gọn"] = df["Phương pháp"].apply(shorten_name)
        df["Tỷ lệ nén (%)"] = (df["Từ (Tóm tắt)"] / df["Từ (Gốc)"]) * 100
        
        st.subheader("📈 Phân tích Các Chỉ Số Học Thuật")
        
        c1, c2 = st.columns(2)
        with c1:
            st.markdown("**1. Tốc độ xử lý (giây)**")
            # THÊM CHÚ THÍCH GIẢI THÍCH BIỂU ĐỒ TỐC ĐỘ
            st.caption("⏳ Cột càng **THẤP** (thời gian ngắn) chứng tỏ AI chạy càng nhanh. Cột cao thể hiện độ trễ lớn, cần nhiều thời gian chờ đợi.")
            fig1 = px.bar(df.groupby("Tên rút gọn")["Thời gian xử lý (s)"].mean().reset_index(), x="Tên rút gọn", y="Thời gian xử lý (s)", text_auto='.2f', color="Tên rút gọn")
            fig1.update_layout(showlegend=False, xaxis_title="")
            st.plotly_chart(fig1, use_container_width=True)
            
        with c2:
            st.markdown("**2. Độ Sáng tạo - Novelty (%)**")
            # THÊM CHÚ THÍCH GIẢI THÍCH BIỂU ĐỒ NOVELTY
            st.caption("🧠 Cột càng **CAO** chứng tỏ AI có khả năng dùng từ vựng mới để viết lại câu (Paraphrase) càng tốt. TextRank luôn = 0 vì thuật toán này chỉ copy-paste câu gốc.")
            fig2 = px.bar(df.groupby("Tên rút gọn")["Novelty (%)"].mean().reset_index(), x="Tên rút gọn", y="Novelty (%)", text_auto='.1f', color="Tên rút gọn")
            fig2.update_layout(showlegend=False, xaxis_title="")
            st.plotly_chart(fig2, use_container_width=True)

        st.markdown("---")
        c3, c4 = st.columns([2, 1])
        with c3:
            st.markdown("**3. Điểm Chuẩn ROUGE-L (%)**")
            # THÊM CHÚ THÍCH GIẢI THÍCH BIỂU ĐỒ ROUGE
            st.caption("🎯 Thanh càng **DÀI** (tỉ lệ cao) chứng tỏ cách hành văn của AI càng sát với bản tóm tắt chuẩn của con người. (Chỉ vẽ biểu đồ khi bạn có nhập Bản tóm tắt mẫu).")
            df_rouge = df[df["ROUGE-L (%)"] > 0]
            if not df_rouge.empty:
                fig3 = px.bar(df_rouge.groupby("Tên rút gọn")["ROUGE-L (%)"].mean().reset_index(), y="Tên rút gọn", x="ROUGE-L (%)", orientation='h', text_auto='.1f', color="Tên rút gọn")
                fig3.update_layout(showlegend=False, yaxis_title="")
                st.plotly_chart(fig3, use_container_width=True)
            else:
                st.info("💡 Bạn chưa nhập 'Bản tóm tắt chuẩn' lần nào nên chưa có biểu đồ ROUGE.")
                
        with c4:
            st.markdown("**4. Tỷ lệ nén văn bản (%)**")
            # THÊM CHÚ THÍCH GIẢI THÍCH TỶ LỆ NÉN
            st.caption("📦 Phần trăm số từ của bản tóm tắt so với bản gốc. Miếng bánh **NHỎ** nghĩa là AI tóm tắt siêu ngắn gọn. Miếng bánh **TO** là AI giữ lại nhiều chi tiết.")
            fig4 = px.pie(df.groupby("Tên rút gọn")["Tỷ lệ nén (%)"].mean().reset_index(), values="Tỷ lệ nén (%)", names="Tên rút gọn", hole=0.4)
            st.plotly_chart(fig4, use_container_width=True)
            
        st.markdown("---")
        st.subheader("📚 Bảng dữ liệu SQLite (Đã lưu điểm học thuật)")
        st.dataframe(df.drop(columns=["Văn bản gốc", "Kết quả", "Tên rút gọn", "Tỷ lệ nén (%)"], errors='ignore'), use_container_width=True)