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