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