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)