Spaces:
Sleeping
Sleeping
| import re | |
| import numpy as np | |
| from underthesea import sent_tokenize | |
| class ChunkUndertheseaBuilder: | |
| """ | |
| Bộ tách văn bản tiếng Việt thông minh: | |
| 1️⃣ Lọc trước (Extractive): chỉ giữ các câu có ý chính | |
| 2️⃣ Gộp sau (Semantic): nhóm các câu trọng tâm theo ngữ nghĩa | |
| """ | |
| def __init__(self, | |
| embedder, | |
| device: str = "cpu", | |
| min_words: int = 256, | |
| max_words: int = 768, | |
| sim_threshold: float = 0.7, | |
| key_sent_ratio: float = 0.4): | |
| if embedder is None: | |
| raise ValueError("❌ Cần truyền mô hình embedder đã load sẵn.") | |
| self.embedder = embedder | |
| self.device = device | |
| self.min_words = min_words | |
| self.max_words = max_words | |
| self.sim_threshold = sim_threshold | |
| self.key_sent_ratio = key_sent_ratio | |
| # ============================================================ | |
| # 1️⃣ Tách câu | |
| # ============================================================ | |
| def _split_sentences(self, text: str): | |
| """Tách câu tiếng Việt (fallback nếu underthesea lỗi).""" | |
| text = re.sub(r"[\x00-\x1f]+", " ", text) | |
| try: | |
| sents = sent_tokenize(text) | |
| except Exception: | |
| sents = re.split(r"(?<=[.!?])\s+", text) | |
| return [s.strip() for s in sents if len(s.strip()) > 2] | |
| # ============================================================ | |
| # 2️⃣ Encode an toàn (GPU/CPU fallback) | |
| # ============================================================ | |
| def _encode(self, sentences): | |
| try: | |
| return self.embedder.encode( | |
| sentences, | |
| convert_to_numpy=True, | |
| show_progress_bar=False, | |
| device=str(self.device) | |
| ) | |
| except TypeError: | |
| return self.embedder.encode(sentences, convert_to_numpy=True, show_progress_bar=False) | |
| except RuntimeError as e: | |
| if "CUDA" in str(e): | |
| print("⚠️ GPU OOM, fallback sang CPU.") | |
| return self.embedder.encode( | |
| sentences, convert_to_numpy=True, show_progress_bar=False, device="cpu" | |
| ) | |
| raise e | |
| # ============================================================ | |
| # 3️⃣ Lọc ý chính trước (EXTRACTIVE) | |
| # ============================================================ | |
| def _extractive_filter(self, sentences): | |
| """Chọn ra top-k câu đại diện nội dung nhất.""" | |
| if len(sentences) <= 3: | |
| return sentences | |
| embeddings = self._encode(sentences) | |
| mean_vec = np.mean(embeddings, axis=0) | |
| sims = np.dot(embeddings, mean_vec) / ( | |
| np.linalg.norm(embeddings, axis=1) * np.linalg.norm(mean_vec) | |
| ) | |
| # Chọn top-k câu có similarity cao nhất | |
| k = max(1, int(len(sentences) * self.key_sent_ratio)) | |
| idx = np.argsort(-sims)[:k] | |
| idx.sort() # giữ thứ tự gốc | |
| selected = [sentences[i] for i in idx] | |
| return selected | |
| # ============================================================ | |
| # 4️⃣ Gộp các câu trọng tâm theo ngữ nghĩa | |
| # ============================================================ | |
| def _semantic_group(self, sentences): | |
| """Gộp các câu đã lọc theo mức tương đồng ngữ nghĩa.""" | |
| if not sentences: | |
| return [] | |
| embeddings = self._encode(sentences) | |
| embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True) | |
| chunks, cur_chunk, cur_len = [], [], 0 | |
| for i, sent in enumerate(sentences): | |
| wc = len(sent.split()) | |
| if not cur_chunk: | |
| cur_chunk.append(sent) | |
| cur_len = wc | |
| continue | |
| sim = np.dot(embeddings[i - 1], embeddings[i]) | |
| too_long = cur_len + wc > self.max_words | |
| too_short = cur_len < self.min_words | |
| topic_changed = sim < self.sim_threshold | |
| if too_long or (not too_short and topic_changed): | |
| chunks.append(" ".join(cur_chunk)) | |
| cur_chunk = [sent] | |
| cur_len = wc | |
| else: | |
| cur_chunk.append(sent) | |
| cur_len += wc | |
| if cur_chunk: | |
| chunks.append(" ".join(cur_chunk)) | |
| return chunks | |
| # ============================================================ | |
| # 5️⃣ Hàm chính build() | |
| # ============================================================ | |
| def build(self, full_text: str): | |
| """ | |
| Trả về list chứa {Index, Content} cho từng chunk. | |
| Quy trình: | |
| - Lọc câu trọng tâm trước | |
| - Gộp các câu đã lọc theo ngữ nghĩa | |
| """ | |
| all_sentences = self._split_sentences(full_text) | |
| print(f"📄 Tổng số câu: {len(all_sentences)}") | |
| # --- Bước 1: lọc ý chính --- | |
| filtered = self._extractive_filter(all_sentences) | |
| print(f"✨ Giữ lại {len(filtered)} câu (~{len(filtered)/len(all_sentences):.0%}) sau extractive filter") | |
| # --- Bước 2: gộp thành các đoạn ngữ nghĩa --- | |
| chunks = self._semantic_group(filtered) | |
| results = [{"Index": i, "Content": chunk} for i, chunk in enumerate(chunks, start=1)] | |
| print(f"🔹 Tạo {len(results)} chunk ngữ nghĩa từ {len(filtered)} câu trọng tâm.") | |
| return results | |