thinh21 commited on
Commit
a9b0b3a
·
verified ·
1 Parent(s): 16cccc3

Upload 10 files

Browse files
api_keys.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # File này chỉ lưu trữ trên máy tính của bạn, KHÔNG đẩy lên GitHub
2
+ GEMINI_KEY = "AIzaSyBJWldClO1-5ANcxVfQcHk2oHZxsRUChw4"
3
+ GROQ_KEY = "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" # Dán mã Key bạn vừa tạo vào đây
4
+ COHERE_KEY = "poK0FNYgmnNbYNlwf16SDG6DAlwOlmELKD2gXt6f"
5
+ OPENROUTER_KEY = "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" # Dán key của bạn vào đây
app.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import PyPDF2
3
+ import docx
4
+ import time
5
+ import pandas as pd
6
+ from io import BytesIO
7
+ from rouge_score import rouge_scorer # <-- [MỚI] Thư viện tính điểm học thuật
8
+ import plotly.express as px
9
+ import os
10
+ import smtplib
11
+ from email.message import EmailMessage
12
+
13
+ # --- IMPORT CÁC MODULE XỬ LÝ ---
14
+ from summarizer_ai import TextSummarizer
15
+ from textrank_summarizer import TextRankSummarizer
16
+ from text_cleaner import TextPreprocessor
17
+ from groq_summarizer import GroqSummarizer
18
+ from cohere_summarizer import CohereSummarizer
19
+ import database
20
+ import api_keys
21
+
22
+ st.set_page_config(page_title="AI Summarizer Pro", page_icon="📝", layout="wide")
23
+ database.init_db()
24
+
25
+ @st.cache_resource
26
+ def load_models():
27
+ return (
28
+ TextSummarizer(), TextRankSummarizer(), TextPreprocessor(),
29
+ GroqSummarizer(api_keys.GROQ_KEY), CohereSummarizer(api_keys.COHERE_KEY)
30
+ )
31
+
32
+ def _ensure_auth_state():
33
+ if "user" not in st.session_state:
34
+ st.session_state.user = None
35
+
36
+ def _mask_email(email: str) -> str:
37
+ email = (email or "").strip()
38
+ if "@" not in email:
39
+ return "***"
40
+ name, domain = email.split("@", 1)
41
+ if len(name) <= 2:
42
+ name_masked = name[:1] + "*"
43
+ else:
44
+ name_masked = name[:2] + "*" * (len(name) - 2)
45
+ return f"{name_masked}@{domain}"
46
+
47
+ def _send_reset_email(to_email: str, code: str):
48
+ smtp_user = os.getenv("SMTP_USER", "").strip() or str(st.secrets.get("SMTP_USER", "")).strip()
49
+ smtp_app_password = os.getenv("SMTP_APP_PASSWORD", "").strip() or str(st.secrets.get("SMTP_APP_PASSWORD", "")).strip()
50
+ if not smtp_user or not smtp_app_password:
51
+ return False, "Chưa cấu hình SMTP. Hãy set SMTP_USER/SMTP_APP_PASSWORD (env hoặc .streamlit/secrets.toml)."
52
+
53
+ msg = EmailMessage()
54
+ msg["Subject"] = "AI Summarizer Pro - Ma dat lai mat khau"
55
+ msg["From"] = smtp_user
56
+ msg["To"] = to_email
57
+ msg.set_content(
58
+ "Ban da yeu cau dat lai mat khau.\n\n"
59
+ f"Ma xac nhan (OTP): {code}\n"
60
+ "Ma co hieu luc 10 phut. Neu khong phai ban, hay bo qua email nay.\n"
61
+ )
62
+
63
+ try:
64
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
65
+ server.login(smtp_user, smtp_app_password)
66
+ server.send_message(msg)
67
+ return True, "Da gui ma OTP qua email."
68
+ except Exception as e:
69
+ return False, f"Gui email that bai: {e}"
70
+
71
+ def _render_auth_sidebar():
72
+ st.sidebar.header("👤 Tài khoản")
73
+
74
+ if st.session_state.user:
75
+ st.sidebar.success(f"Xin chào, {st.session_state.user['username']}")
76
+ if st.sidebar.button("Đăng xuất"):
77
+ st.session_state.user = None
78
+ st.rerun()
79
+ return True
80
+
81
+ tab_login, tab_register, tab_forgot = st.sidebar.tabs(["Đăng nhập", "Đăng ký", "Quên mật khẩu"])
82
+
83
+ with tab_login:
84
+ with st.form("login_form", clear_on_submit=False):
85
+ username = st.text_input("Username", placeholder="vd: thinh")
86
+ password = st.text_input("Password", type="password")
87
+ submitted = st.form_submit_button("Đăng nhập", type="primary")
88
+ if submitted:
89
+ ok, user, msg = database.authenticate_user(username, password)
90
+ if ok:
91
+ st.session_state.user = user
92
+ st.sidebar.success(msg)
93
+ st.rerun()
94
+ else:
95
+ st.sidebar.error(msg)
96
+
97
+ with tab_register:
98
+ with st.form("register_form", clear_on_submit=True):
99
+ username = st.text_input("Username (bắt buộc)")
100
+ email = st.text_input("Email (tuỳ chọn)", placeholder="name@example.com")
101
+ password = st.text_input("Password (bắt buộc)", type="password")
102
+ confirm = st.text_input("Nhập lại password", type="password")
103
+ submitted = st.form_submit_button("Tạo tài khoản", type="primary")
104
+ if submitted:
105
+ if password != confirm:
106
+ st.sidebar.error("Password nhập lại không khớp.")
107
+ elif len((password or "")) < 6:
108
+ st.sidebar.error("Password tối thiểu 6 ký tự.")
109
+ else:
110
+ ok, msg = database.create_user(username=username, password=password, email=email)
111
+ if ok:
112
+ st.sidebar.success(msg)
113
+ else:
114
+ st.sidebar.error(msg)
115
+
116
+ with tab_forgot:
117
+ st.caption("Nhập username hoặc email đã đăng ký để nhận mã OTP.")
118
+ smtp_user_present = bool(os.getenv("SMTP_USER", "").strip() or str(st.secrets.get("SMTP_USER", "")).strip())
119
+ smtp_pass_present = bool(os.getenv("SMTP_APP_PASSWORD", "").strip() or str(st.secrets.get("SMTP_APP_PASSWORD", "")).strip())
120
+ if not (smtp_user_present and smtp_pass_present):
121
+ st.warning("SMTP chưa được cấu hình cho phiên chạy hiện tại.")
122
+
123
+ with st.form("forgot_request_form", clear_on_submit=True):
124
+ identifier = st.text_input("Username hoặc Email")
125
+ submitted = st.form_submit_button("Gửi mã OTP", type="primary")
126
+
127
+ if submitted:
128
+ ok, email, code_or_msg = database.create_password_reset_code(identifier)
129
+ if ok:
130
+ send_ok, send_msg = _send_reset_email(email, code_or_msg)
131
+ if send_ok:
132
+ st.sidebar.success(f"{send_msg} ({_mask_email(email)})")
133
+ else:
134
+ st.sidebar.error(send_msg)
135
+ else:
136
+ st.sidebar.info(code_or_msg)
137
+
138
+ st.divider()
139
+ st.caption("Sau khi nhận OTP, nhập mã và mật khẩu mới.")
140
+
141
+ with st.form("forgot_reset_form", clear_on_submit=True):
142
+ identifier2 = st.text_input("Username hoặc Email (để đặt lại)")
143
+ code = st.text_input("Mã OTP (6 số)")
144
+ new_password = st.text_input("Mật khẩu mới", type="password")
145
+ confirm = st.text_input("Nhập lại mật khẩu mới", type="password")
146
+ submitted2 = st.form_submit_button("Đổi mật khẩu", type="primary")
147
+
148
+ if submitted2:
149
+ if new_password != confirm:
150
+ st.sidebar.error("Password nhập lại không khớp.")
151
+ else:
152
+ ok2, msg2 = database.reset_password_with_code(identifier2, code, new_password)
153
+ if ok2:
154
+ st.sidebar.success(msg2)
155
+ else:
156
+ st.sidebar.error(msg2)
157
+
158
+ return False
159
+
160
+ # ==========================================
161
+ # HÀM TÍNH TOÁN CÁC ĐỘ ĐO (METRICS)
162
+ # ==========================================
163
+ def calc_novelty(original_text, summary_text):
164
+ """Tính tỷ lệ phần trăm từ vựng mới được AI tạo ra (Độ sáng tạo)"""
165
+ orig_set = set(original_text.lower().split())
166
+ summ_set = set(summary_text.lower().split())
167
+ if not summ_set: return 0.0
168
+ new_words = summ_set - orig_set
169
+ return round((len(new_words) / len(summ_set)) * 100, 1)
170
+
171
+ def calc_rouge_l(reference_text, summary_text):
172
+ """Tính điểm ROUGE-L (Mức độ hành văn giống với con người)"""
173
+ if not reference_text.strip(): return 0.0
174
+ scorer = rouge_scorer.RougeScorer(['rougeL'], use_stemmer=False)
175
+ scores = scorer.score(reference_text, summary_text)
176
+ return round(scores['rougeL'].fmeasure * 100, 1)
177
+
178
+ def extract_text_from_file(uploaded_file):
179
+ try:
180
+ filename = uploaded_file.name
181
+ if filename.endswith('.txt'): return uploaded_file.getvalue().decode("utf-8")
182
+ elif filename.endswith('.pdf'):
183
+ pdf_reader = PyPDF2.PdfReader(BytesIO(uploaded_file.read()))
184
+ return "".join([page.extract_text() + "\n" for page in pdf_reader.pages if page.extract_text()])
185
+ elif filename.endswith('.docx'):
186
+ doc = docx.Document(BytesIO(uploaded_file.read()))
187
+ return "\n".join([para.text for para in doc.paragraphs])
188
+ except Exception as e:
189
+ st.error(f"Lỗi đọc file: {e}")
190
+ return ""
191
+
192
+ # ==========================================
193
+ # GIAO DIỆN CHÍNH
194
+ # ==========================================
195
+ _ensure_auth_state()
196
+ is_authed = _render_auth_sidebar()
197
+
198
+ st.title("📝 Hệ thống Tóm tắt & Nghiên cứu Đánh giá AI")
199
+ 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.")
200
+
201
+ if not is_authed:
202
+ st.info("Vui lòng đăng nhập hoặc đăng ký ở sidebar để sử dụng hệ thống.")
203
+ st.stop()
204
+
205
+ (ai_summarizer, textrank_summarizer, text_cleaner, groq_summarizer, cohere_summarizer) = load_models()
206
+
207
+ st.sidebar.header("⚙️ Cấu hình chung")
208
+ summary_length = st.sidebar.slider("Độ dài tóm tắt mong muốn (số từ):", 30, 1000, 100)
209
+
210
+ st.subheader("📥 Dữ liệu đầu vào")
211
+ uploaded_file = st.file_uploader("📂 Tải lên tài liệu (PDF, DOCX, TXT)", type=["pdf", "docx", "txt"])
212
+ input_content = extract_text_from_file(uploaded_file) if uploaded_file else ""
213
+
214
+ c_input, c_ref = st.columns(2)
215
+ with c_input:
216
+ 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)
217
+ with c_ref:
218
+ 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...")
219
+
220
+ cleaned_text = text_cleaner.clean_text(input_text)
221
+ original_word_count = len(cleaned_text.split())
222
+
223
+ tab1, tab2, tab3 = st.tabs(["📝 Tóm tắt Đơn", "⚖️ So sánh Đa mô hình", "📊 Dashboard & Lịch sử DB"])
224
+
225
+ # ---------------------------------------------------------
226
+ # TAB 1: TÓM TẮT ĐƠN
227
+ # ---------------------------------------------------------
228
+ with tab1:
229
+ method = st.selectbox("Chọn mô hình AI:", [
230
+ "Thông minh (AI T5 - Viết lại câu)", "Trích xuất ý chính (TextRank)",
231
+ "⚡ Siêu tốc độ (Groq Llama 3 API)", "🌟 Tóm tắt chuyên sâu (Cohere API)"
232
+ ])
233
+
234
+ if st.button("🚀 Chạy Mô hình Đơn", type="primary"):
235
+ if original_word_count < 20: st.warning("⚠️ Văn bản quá ngắn.")
236
+ else:
237
+ with st.spinner(f"🤖 Đang xử lý bằng {method}..."):
238
+ start_time = time.time()
239
+ try:
240
+ if "T5" in method: result = ai_summarizer.summarize(cleaned_text, max_len=summary_length)
241
+ elif "Groq" in method: result = groq_summarizer.summarize(cleaned_text, max_words=summary_length)
242
+ elif "Cohere" in method: result = cohere_summarizer.summarize(cleaned_text, max_words=summary_length)
243
+ else: result = textrank_summarizer.summarize(cleaned_text, num_sentences=max(1, summary_length // 20))
244
+
245
+ p_time = round(time.time() - start_time, 2)
246
+ sum_count = len(result.split())
247
+ novelty = calc_novelty(cleaned_text, result)
248
+ rouge = calc_rouge_l(reference_text, result)
249
+
250
+ st.success(result)
251
+ if not result.startswith("⚠️"):
252
+ database.save_summary(method, original_word_count, sum_count, p_time, cleaned_text, result, novelty, rouge)
253
+
254
+ m1, m2, m3, m4 = st.columns(4)
255
+ m1.metric("⏱️ Thời gian", f"{p_time}s")
256
+ m2.metric("📉 Tỷ lệ nén", f"{round((sum_count/original_word_count)*100, 1)}%")
257
+ m3.metric("🧠 Độ sáng tạo (Novelty)", f"{novelty}%")
258
+ m4.metric("🎯 Điểm ROUGE-L", f"{rouge}%" if reference_text else "N/A")
259
+ except Exception as e:
260
+ st.error(f"Lỗi: {e}")
261
+
262
+ # ---------------------------------------------------------
263
+ # TAB 2: SO SÁNH ĐA MÔ HÌNH
264
+ # ---------------------------------------------------------
265
+ with tab2:
266
+ 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.")
267
+ if st.button("⚖️ Bắt đầu Đại chiến AI (Chạy tất cả)", type="primary"):
268
+ if original_word_count < 20: st.warning("⚠️ Văn bản quá ngắn.")
269
+ else:
270
+ col1, col2 = st.columns(2)
271
+ col3, col4 = st.columns(2)
272
+
273
+ def render_result(col, title, res, time_taken, method_name):
274
+ with col:
275
+ st.markdown(f"### {title}")
276
+ st.write(res)
277
+ if not res.startswith("⚠️"):
278
+ sum_cnt = len(res.split())
279
+ nov = calc_novelty(cleaned_text, res)
280
+ rg = calc_rouge_l(reference_text, res)
281
+ st.caption(f"⏱️ {time_taken}s | 📝 {sum_cnt} từ | 🧠 Novelty: {nov}% | 🎯 ROUGE: {rg if reference_text else 'N/A'}")
282
+ database.save_summary(method_name, original_word_count, sum_cnt, time_taken, cleaned_text, res, nov, rg)
283
+
284
+ # 1. Llama 3 (Groq)
285
+ start_t = time.time()
286
+ res_groq = groq_summarizer.summarize(cleaned_text, max_words=summary_length)
287
+ render_result(col1, "⚡ Groq (Llama 3)", res_groq, round(time.time() - start_t, 2), "⚡ Siêu tốc độ (Groq Llama 3 API)")
288
+
289
+ # 2. Cohere
290
+ start_t = time.time()
291
+ res_co = cohere_summarizer.summarize(cleaned_text, max_words=summary_length)
292
+ render_result(col2, "🌟 Cohere API", res_co, round(time.time() - start_t, 2), "🌟 Tóm tắt chuyên sâu (Cohere API)")
293
+
294
+ # 3. T5 Local
295
+ start_t = time.time()
296
+ res_t5 = ai_summarizer.summarize(cleaned_text, max_len=summary_length)
297
+ 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)")
298
+
299
+ # 4. TextRank
300
+ start_t = time.time()
301
+ res_tr = textrank_summarizer.summarize(cleaned_text, num_sentences=max(1, summary_length // 20))
302
+ render_result(col4, "✂️ TextRank", res_tr, round(time.time() - start_t, 2), "Trích xuất ý chính (TextRank)")
303
+
304
+ # ---------------------------------------------------------
305
+ # TAB 3: THỐNG KÊ & BIỂU ĐỒ
306
+ # ---------------------------------------------------------
307
+ with tab3:
308
+ history_data = database.get_history()
309
+ if len(history_data) == 0:
310
+ st.write("Chưa có dữ liệu. Hãy chạy tóm tắt vài lần để xem biểu đồ!")
311
+ else:
312
+ 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 (%)"])
313
+
314
+ def shorten_name(name):
315
+ if "T5" in name: return "AI T5 (Local)"
316
+ if "TextRank" in name: return "TextRank"
317
+ if "Groq" in name: return "Groq Llama 3"
318
+ if "Cohere" in name: return "Cohere"
319
+ return name
320
+
321
+ df["Tên rút gọn"] = df["Phương pháp"].apply(shorten_name)
322
+ df["Tỷ lệ nén (%)"] = (df["Từ (Tóm tắt)"] / df["Từ (Gốc)"]) * 100
323
+
324
+ st.subheader("📈 Phân tích Các Chỉ Số Học Thuật")
325
+
326
+ c1, c2 = st.columns(2)
327
+ with c1:
328
+ st.markdown("**1. Tốc độ xử lý (giây)**")
329
+ # THÊM CHÚ THÍCH GIẢI THÍCH BIỂU ĐỒ TỐC ĐỘ
330
+ 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.")
331
+ 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")
332
+ fig1.update_layout(showlegend=False, xaxis_title="")
333
+ st.plotly_chart(fig1, use_container_width=True)
334
+
335
+ with c2:
336
+ st.markdown("**2. Độ Sáng tạo - Novelty (%)**")
337
+ # THÊM CHÚ THÍCH GIẢI THÍCH BIỂU ĐỒ NOVELTY
338
+ 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.")
339
+ 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")
340
+ fig2.update_layout(showlegend=False, xaxis_title="")
341
+ st.plotly_chart(fig2, use_container_width=True)
342
+
343
+ st.markdown("---")
344
+ c3, c4 = st.columns([2, 1])
345
+ with c3:
346
+ st.markdown("**3. Điểm Chuẩn ROUGE-L (%)**")
347
+ # THÊM CHÚ THÍCH GIẢI THÍCH BIỂU ĐỒ ROUGE
348
+ 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).")
349
+ df_rouge = df[df["ROUGE-L (%)"] > 0]
350
+ if not df_rouge.empty:
351
+ 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")
352
+ fig3.update_layout(showlegend=False, yaxis_title="")
353
+ st.plotly_chart(fig3, use_container_width=True)
354
+ else:
355
+ 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.")
356
+
357
+ with c4:
358
+ st.markdown("**4. Tỷ lệ nén văn bản (%)**")
359
+ # THÊM CHÚ THÍCH GIẢI THÍCH TỶ LỆ NÉN
360
+ 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.")
361
+ 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)
362
+ st.plotly_chart(fig4, use_container_width=True)
363
+
364
+ st.markdown("---")
365
+ st.subheader("📚 Bảng dữ liệu SQLite (Đã lưu điểm học thuật)")
366
+ 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)
cohere_summarizer.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cohere
2
+
3
+ class CohereSummarizer:
4
+ def __init__(self, api_key):
5
+ # Sử dụng Client V2 để tương thích với hệ thống mới nhất 2026
6
+ self.co = cohere.Client(api_key)
7
+
8
+ def summarize(self, text, max_words=100):
9
+ try:
10
+ # Chuyển hẳn sang dùng Chat API vì Summarize API cũ không còn được hỗ trợ
11
+ response = self.co.chat(
12
+ model='command-r-plus-08-2024', # Dùng phiên bản ổn định nhất hiện nay
13
+ message=f"Tóm tắt văn bản sau bằng tiếng Việt, khoảng {max_words} từ: {text}",
14
+ )
15
+ return response.text.strip()
16
+ except Exception as e:
17
+ # Trình bày lỗi gọn gàng cho đồ án
18
+ return f"⚠️ Lỗi Cohere (New API): {str(e)}"
config.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # ==========================================
2
+ # CẤU HÌNH HỆ THỐNG
3
+ # ==========================================
4
+
5
+ # Sử dụng mô hình của Đại học Bách Khoa Hà Nội (Tương thích tốt hơn)
6
+ MODEL_NAME = "NlpHUST/t5-small-vi-summarization"
7
+
8
+ MAX_LENGTH = 300
9
+ MIN_LENGTH = 30
database.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ from datetime import datetime, timedelta
3
+ import hashlib
4
+ import hmac
5
+ import os
6
+ import secrets
7
+
8
+ _DB_PATH = "history.db"
9
+ _PWD_ITERATIONS = 200_000
10
+ _RESET_CODE_ITERATIONS = 120_000
11
+ _RESET_CODE_TTL_MINUTES = 10
12
+ _RESET_MAX_ATTEMPTS = 5
13
+
14
+ def _utc_now_str():
15
+ return datetime.utcnow().strftime("%d/%m/%Y %H:%M:%S")
16
+
17
+ def _hash_password(password: str, salt: bytes) -> bytes:
18
+ return hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, _PWD_ITERATIONS)
19
+
20
+ def _hash_reset_code(code: str, salt: bytes) -> bytes:
21
+ return hashlib.pbkdf2_hmac("sha256", code.encode("utf-8"), salt, _RESET_CODE_ITERATIONS)
22
+
23
+ def init_db():
24
+ conn = sqlite3.connect(_DB_PATH)
25
+ c = conn.cursor()
26
+ c.execute('''
27
+ CREATE TABLE IF NOT EXISTS summary_history (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ created_at TEXT,
30
+ method TEXT,
31
+ original_length INTEGER,
32
+ summary_length INTEGER,
33
+ process_time REAL,
34
+ original_text TEXT,
35
+ summary_text TEXT,
36
+ novelty_score REAL,
37
+ rouge_l_score REAL
38
+ )
39
+ ''')
40
+
41
+ c.execute('''
42
+ CREATE TABLE IF NOT EXISTS users (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ created_at TEXT NOT NULL,
45
+ username TEXT NOT NULL UNIQUE,
46
+ email TEXT,
47
+ password_salt TEXT NOT NULL,
48
+ password_hash TEXT NOT NULL
49
+ )
50
+ ''')
51
+
52
+ c.execute('''
53
+ CREATE TABLE IF NOT EXISTS password_reset_codes (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ created_at TEXT NOT NULL,
56
+ user_id INTEGER NOT NULL,
57
+ code_salt TEXT NOT NULL,
58
+ code_hash TEXT NOT NULL,
59
+ expires_at TEXT NOT NULL,
60
+ attempts INTEGER NOT NULL DEFAULT 0,
61
+ used INTEGER NOT NULL DEFAULT 0,
62
+ FOREIGN KEY(user_id) REFERENCES users(id)
63
+ )
64
+ ''')
65
+ conn.commit()
66
+ conn.close()
67
+
68
+ def save_summary(method, orig_len, sum_len, p_time, orig_text, sum_text, novelty=0.0, rouge_l=0.0):
69
+ conn = sqlite3.connect(_DB_PATH)
70
+ c = conn.cursor()
71
+ date_str = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
72
+ c.execute('''
73
+ INSERT INTO summary_history
74
+ (created_at, method, original_length, summary_length, process_time, original_text, summary_text, novelty_score, rouge_l_score)
75
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
76
+ ''', (date_str, method, orig_len, sum_len, p_time, orig_text, sum_text, novelty, rouge_l))
77
+ conn.commit()
78
+ conn.close()
79
+
80
+ def get_history():
81
+ conn = sqlite3.connect(_DB_PATH)
82
+ c = conn.cursor()
83
+ c.execute("SELECT * FROM summary_history ORDER BY id DESC LIMIT 50")
84
+ rows = c.fetchall()
85
+ conn.close()
86
+ return rows
87
+
88
+ def create_user(username: str, password: str, email: str | None = None):
89
+ username = (username or "").strip()
90
+ email = (email or "").strip() or None
91
+ if not username or not password:
92
+ return False, "Thiếu username hoặc password."
93
+
94
+ salt = os.urandom(16)
95
+ pwd_hash = _hash_password(password, salt)
96
+
97
+ try:
98
+ conn = sqlite3.connect(_DB_PATH)
99
+ c = conn.cursor()
100
+ c.execute(
101
+ "INSERT INTO users (created_at, username, email, password_salt, password_hash) VALUES (?, ?, ?, ?, ?)",
102
+ (_utc_now_str(), username, email, salt.hex(), pwd_hash.hex()),
103
+ )
104
+ conn.commit()
105
+ return True, "Đăng ký thành công."
106
+ except sqlite3.IntegrityError:
107
+ return False, "Username đã tồn tại."
108
+ finally:
109
+ try:
110
+ conn.close()
111
+ except Exception:
112
+ pass
113
+
114
+ def authenticate_user(username: str, password: str):
115
+ username = (username or "").strip()
116
+ if not username or not password:
117
+ return False, None, "Thiếu username hoặc password."
118
+
119
+ conn = sqlite3.connect(_DB_PATH)
120
+ c = conn.cursor()
121
+ c.execute("SELECT id, username, email, password_salt, password_hash FROM users WHERE username = ?", (username,))
122
+ row = c.fetchone()
123
+ conn.close()
124
+
125
+ if not row:
126
+ return False, None, "Sai username hoặc password."
127
+
128
+ user_id, uname, email, salt_hex, hash_hex = row
129
+ salt = bytes.fromhex(salt_hex)
130
+ expected = bytes.fromhex(hash_hex)
131
+ actual = _hash_password(password, salt)
132
+
133
+ if not hmac.compare_digest(expected, actual):
134
+ return False, None, "Sai username hoặc password."
135
+
136
+ return True, {"id": user_id, "username": uname, "email": email}, "Đăng nhập thành công."
137
+
138
+ def _get_user_by_identifier(identifier: str):
139
+ identifier = (identifier or "").strip()
140
+ if not identifier:
141
+ return None
142
+
143
+ conn = sqlite3.connect(_DB_PATH)
144
+ c = conn.cursor()
145
+ if "@" in identifier:
146
+ c.execute("SELECT id, username, email FROM users WHERE email = ?", (identifier,))
147
+ else:
148
+ c.execute("SELECT id, username, email FROM users WHERE username = ?", (identifier,))
149
+ row = c.fetchone()
150
+ conn.close()
151
+ if not row:
152
+ return None
153
+ user_id, username, email = row
154
+ return {"id": user_id, "username": username, "email": email}
155
+
156
+ def create_password_reset_code(identifier: str):
157
+ """
158
+ Create a one-time reset code for a user (by username or email).
159
+ Returns: (ok: bool, email: str|None, msg: str)
160
+ """
161
+ user = _get_user_by_identifier(identifier)
162
+ if not user:
163
+ # Don't reveal whether user exists
164
+ return False, None, "Nếu tài khoản tồn tại và có email, hệ thống sẽ gửi mã đặt lại mật khẩu."
165
+
166
+ email = (user.get("email") or "").strip()
167
+ if not email:
168
+ return False, None, "Tài khoản này chưa có email nên không thể đặt lại mật khẩu."
169
+
170
+ code = f"{secrets.randbelow(1_000_000):06d}"
171
+ salt = os.urandom(16)
172
+ code_hash = _hash_reset_code(code, salt)
173
+
174
+ now = datetime.utcnow()
175
+ expires = now + timedelta(minutes=_RESET_CODE_TTL_MINUTES)
176
+
177
+ conn = sqlite3.connect(_DB_PATH)
178
+ c = conn.cursor()
179
+ # Invalidate previous unused codes for this user
180
+ c.execute("UPDATE password_reset_codes SET used = 1 WHERE user_id = ? AND used = 0", (user["id"],))
181
+ c.execute(
182
+ "INSERT INTO password_reset_codes (created_at, user_id, code_salt, code_hash, expires_at, attempts, used) VALUES (?, ?, ?, ?, ?, 0, 0)",
183
+ (_utc_now_str(), user["id"], salt.hex(), code_hash.hex(), expires.isoformat()),
184
+ )
185
+ conn.commit()
186
+ conn.close()
187
+
188
+ return True, email, code
189
+
190
+ def reset_password_with_code(identifier: str, code: str, new_password: str):
191
+ """
192
+ Verify reset code and update password.
193
+ Returns: (ok: bool, msg: str)
194
+ """
195
+ user = _get_user_by_identifier(identifier)
196
+ if not user:
197
+ return False, "Mã không hợp lệ hoặc đã hết hạn."
198
+
199
+ code = (code or "").strip()
200
+ if not code or not new_password:
201
+ return False, "Thiếu mã hoặc mật khẩu mới."
202
+ if len(new_password) < 6:
203
+ return False, "Password tối thiểu 6 ký tự."
204
+
205
+ conn = sqlite3.connect(_DB_PATH)
206
+ c = conn.cursor()
207
+ c.execute(
208
+ """
209
+ SELECT id, code_salt, code_hash, expires_at, attempts, used
210
+ FROM password_reset_codes
211
+ WHERE user_id = ?
212
+ ORDER BY id DESC
213
+ LIMIT 1
214
+ """,
215
+ (user["id"],),
216
+ )
217
+ row = c.fetchone()
218
+ if not row:
219
+ conn.close()
220
+ return False, "Mã không hợp lệ hoặc đã hết hạn."
221
+
222
+ reset_id, salt_hex, hash_hex, expires_at, attempts, used = row
223
+ if used:
224
+ conn.close()
225
+ return False, "Mã không hợp lệ hoặc đã hết hạn."
226
+
227
+ try:
228
+ expires_dt = datetime.fromisoformat(expires_at)
229
+ except Exception:
230
+ expires_dt = datetime.utcnow() - timedelta(days=1)
231
+
232
+ if datetime.utcnow() > expires_dt:
233
+ c.execute("UPDATE password_reset_codes SET used = 1 WHERE id = ?", (reset_id,))
234
+ conn.commit()
235
+ conn.close()
236
+ return False, "Mã không hợp lệ hoặc đã hết hạn."
237
+
238
+ if attempts >= _RESET_MAX_ATTEMPTS:
239
+ c.execute("UPDATE password_reset_codes SET used = 1 WHERE id = ?", (reset_id,))
240
+ conn.commit()
241
+ conn.close()
242
+ return False, "Bạn đã nhập sai quá nhiều lần. Vui lòng yêu cầu mã mới."
243
+
244
+ salt = bytes.fromhex(salt_hex)
245
+ expected = bytes.fromhex(hash_hex)
246
+ actual = _hash_reset_code(code, salt)
247
+ if not hmac.compare_digest(expected, actual):
248
+ c.execute("UPDATE password_reset_codes SET attempts = attempts + 1 WHERE id = ?", (reset_id,))
249
+ conn.commit()
250
+ conn.close()
251
+ return False, "Mã không hợp lệ hoặc đã hết hạn."
252
+
253
+ # Update password
254
+ pwd_salt = os.urandom(16)
255
+ pwd_hash = _hash_password(new_password, pwd_salt)
256
+ c.execute(
257
+ "UPDATE users SET password_salt = ?, password_hash = ? WHERE id = ?",
258
+ (pwd_salt.hex(), pwd_hash.hex(), user["id"]),
259
+ )
260
+ c.execute("UPDATE password_reset_codes SET used = 1 WHERE id = ?", (reset_id,))
261
+ conn.commit()
262
+ conn.close()
263
+ return True, "Đổi mật khẩu thành công. Bạn có thể đăng nhập lại."
groq_summarizer.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from groq import Groq
2
+
3
+ class GroqSummarizer:
4
+ def __init__(self, api_key):
5
+ self.client = Groq(api_key=api_key)
6
+ # Sử dụng model Llama 3 mới nhất, rất giỏi tiếng Việt
7
+ self.model = "llama-3.3-70b-versatile"
8
+
9
+ def summarize(self, text, max_words=100):
10
+ prompt = f"""
11
+ Bạn là một chuyên gia tóm tắt văn bản tiếng Việt.
12
+ Nhiệm vụ: Tóm tắt văn bản dưới đây một cách súc tích, khoảng {max_words} từ.
13
+ Yêu cầu: Giữ lại thông tin quan trọng nhất, hành văn tự nhiên.
14
+
15
+ Văn bản cần tóm tắt:
16
+ {text}
17
+ """
18
+
19
+ try:
20
+ completion = self.client.chat.completions.create(
21
+ model=self.model,
22
+ messages=[{"role": "user", "content": prompt}],
23
+ temperature=0.5,
24
+ max_tokens=1024
25
+ )
26
+ return completion.choices[0].message.content.strip()
27
+ except Exception as e:
28
+ return f"⚠️ Lỗi Groq API: {str(e)}"
requirements.txt ADDED
Binary file (298 Bytes). View file
 
summarizer_ai.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
2
+ import config
3
+ import torch
4
+
5
+ class TextSummarizer:
6
+ def __init__(self):
7
+ # Avoid printing non-ASCII to Windows consoles (cp1252) which can crash Streamlit.
8
+ # T5 Vietnamese models use SentencePiece (`spiece.model`). Force slow tokenizer to avoid
9
+ # tiktoken conversion path that can mis-detect and crash on Windows.
10
+ self.tokenizer = AutoTokenizer.from_pretrained(config.MODEL_NAME, use_fast=False)
11
+
12
+ self.model = AutoModelForSeq2SeqLM.from_pretrained(config.MODEL_NAME)
13
+
14
+ # Kiểm tra nếu có GPU (CUDA) thì chuyển model sang GPU để chạy nhanh và chính xác hơn
15
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
16
+ self.model.to(self.device)
17
+
18
+ def summarize(self, text, max_len=100):
19
+ """
20
+ Hàm tóm tắt nâng cấp: Đảm bảo thoát ý, không lặp, không cụt câu.
21
+ """
22
+ # KỸ THUẬT PROMPT MỚI: Dẫn dắt AI tập trung vào tóm tắt tiếng Việt chất lượng cao
23
+ prompt_text = f"vietnamese summarization: {text}"
24
+
25
+ inputs = self.tokenizer(
26
+ prompt_text,
27
+ max_length=1024,
28
+ return_tensors="pt",
29
+ truncation=True
30
+ ).to(self.device) # Chuyển dữ liệu vào cùng thiết bị với model
31
+
32
+ # THIẾT LẬP THAM SỐ SINH VĂN BẢN TỐI ƯU
33
+ # Tăng biên độ để AI có không gian chọn từ ngữ hay nhất
34
+ min_target = max(20, max_len - 30)
35
+ max_target = max_len + 40
36
+
37
+ summary_ids = self.model.generate(
38
+ inputs["input_ids"],
39
+ max_length=max_target,
40
+ min_length=min_target,
41
+
42
+ # CHIẾN THUẬT CHẤT LƯỢNG CAO
43
+ num_beams=5, # Tăng lên 5 để AI tìm con đường có nghĩa nhất
44
+ length_penalty=1.2, # Điều chỉnh để câu văn đủ ý, không quá ngắn
45
+ no_repeat_ngram_size=3, # Ngăn lặp lại cụm 3 chữ (giúp câu văn đa dạng)
46
+ repetition_penalty=2.5, # Phạt nặng việc lặp lại ý tứ cũ
47
+
48
+ # Đảm bảo kết thúc chuyên nghiệp
49
+ early_stopping=True,
50
+ forced_eos_token_id=self.tokenizer.eos_token_id
51
+ )
52
+
53
+ # Giải mã
54
+ summary_text = self.tokenizer.decode(summary_ids[0], skip_special_tokens=True)
55
+
56
+ # HẬU XỬ LÝ (POST-PROCESSING): Xử lý lỗi cụt chữ cuối câu
57
+ summary_text = summary_text.strip()
58
+
59
+ # Nếu câu cuối cùng không có dấu kết thúc, ta tìm dấu chấm gần nhất hoặc thêm dấu ba chấm
60
+ valid_endings = ('.', '!', '?', '\"', '”')
61
+ if not summary_text.endswith(valid_endings):
62
+ # Tìm vị trí dấu chấm cuối cùng để cắt bỏ phần chữ bị cụt phía sau
63
+ last_dot = max(summary_text.rfind('.'), summary_text.rfind('!'), summary_text.rfind('?'))
64
+ if last_dot != -1 and len(summary_text) - last_dot < 30: # Nếu đoạn cụt ngắn
65
+ summary_text = summary_text[:last_dot + 1]
66
+ else:
67
+ summary_text += "..." # Nếu không tìm thấy dấu chấm, thêm dấu 3 chấm để báo hiệu còn ý
68
+
69
+ return summary_text
text_cleaner.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ class TextPreprocessor:
4
+ def clean_text(self, text):
5
+ if not text:
6
+ return ""
7
+
8
+ # 1. Xóa các khoảng trắng thừa, dấu xuống dòng, khoảng tab liên tiếp
9
+ text = re.sub(r'\s+', ' ', text)
10
+
11
+ # 2. Xóa các thẻ HTML (nếu lỡ copy từ web có dính code)
12
+ text = re.sub(r'<[^>]+>', '', text)
13
+
14
+ # LƯU Ý: Không dùng lệnh xóa ký tự đặc biệt chung chung ở đây nữa
15
+ # Việc giữ lại các dấu câu (, . - / %) là bắt buộc để ngày tháng, tỉ số không bị dính vào nhau.
16
+
17
+ return text.strip()
textrank_summarizer.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import nltk
2
+ from nltk.tokenize import sent_tokenize
3
+ from sklearn.feature_extraction.text import TfidfVectorizer
4
+ import networkx as nx
5
+
6
+ class TextRankSummarizer:
7
+ def __init__(self):
8
+ # Tải bộ tách câu của NLTK (Đã cập nhật thêm punkt_tab cho phiên bản mới)
9
+ try:
10
+ nltk.data.find('tokenizers/punkt')
11
+ nltk.data.find('tokenizers/punkt_tab')
12
+ except LookupError:
13
+ nltk.download('punkt')
14
+ nltk.download('punkt_tab')
15
+
16
+ def summarize(self, text, num_sentences=2):
17
+ # 1. Tách đoạn văn thành các câu riêng biệt
18
+ sentences = sent_tokenize(text)
19
+
20
+ if len(sentences) <= num_sentences:
21
+ return text
22
+
23
+ # ĐIỂM CỘNG ĐỒ ÁN: Khai báo danh sách Stop words tiếng Việt cơ bản
24
+ vietnamese_stopwords = [
25
+ "là", "và", "thì", "mà", "của", "các", "có", "để", "những", "một",
26
+ "trong", "với", "cho", "không", "này", "được", "về", "từ", "khi",
27
+ "đã", "đang", "sẽ", "như", "hay", "hoặc", "tại", "nó", "bởi", "ra", "vào"
28
+ ]
29
+
30
+ # 2. Dùng TF-IDF với tính năng loại bỏ Stop words
31
+ vectorizer = TfidfVectorizer(stop_words=vietnamese_stopwords)
32
+ X = vectorizer.fit_transform(sentences)
33
+
34
+ # 3. Tính toán độ tương đồng (Similarity)
35
+ similarity_matrix = (X * X.T).toarray()
36
+
37
+ # 4. Xây dựng Đồ thị (Graph) và chạy PageRank
38
+ nx_graph = nx.from_numpy_array(similarity_matrix)
39
+ scores = nx.pagerank(nx_graph)
40
+
41
+ # 5. Xếp hạng câu và trích xuất
42
+ ranked_sentences = sorted(((scores[i], s) for i, s in enumerate(sentences)), reverse=True)
43
+ top_sentences = [s for score, s in ranked_sentences[:num_sentences]]
44
+
45
+ return " ".join(top_sentences)
46
+
47
+ def extract_keywords(self, text, num_keywords=5):
48
+ """Trích xuất các từ khóa quan trọng nhất từ văn bản"""
49
+ try:
50
+ # Danh sách từ nối không mang ý nghĩa chính (Stopwords) mở rộng
51
+ vietnamese_stopwords = [
52
+ "là", "và", "thì", "mà", "của", "các", "có", "để", "những", "một",
53
+ "trong", "với", "cho", "không", "này", "được", "về", "từ", "khi",
54
+ "đã", "đang", "sẽ", "như", "hay", "hoặc", "tại", "nó", "bởi", "ra", "vào",
55
+ "nhưng", "cũng", "việc", "đến", "ngày", "năm", "người", "theo", "sau"
56
+ ]
57
+
58
+ # Dùng TF-IDF để tìm các từ xuất hiện nhiều và có sức nặng
59
+ vectorizer = TfidfVectorizer(stop_words=vietnamese_stopwords)
60
+ X = vectorizer.fit_transform([text])
61
+
62
+ # Lấy danh sách từ và điểm số
63
+ words = vectorizer.get_feature_names_out()
64
+ scores = X.toarray()[0]
65
+
66
+ # Lọc ra Top 5 từ khóa điểm cao nhất
67
+ top_indices = scores.argsort()[-num_keywords:][::-1]
68
+ keywords = [words[i] for i in top_indices]
69
+
70
+ return keywords
71
+ except Exception as e:
72
+ return []