| import re
|
|
|
|
|
| def normalize_pdf_text(text):
|
| """Pembersih teks hasil ekstraksi PDF."""
|
|
|
|
|
|
|
|
|
| text = re.sub(
|
| r'\b([a-zA-Z](?:\s[a-zA-Z]){2,})\b',
|
| lambda m: m.group(1).replace(' ', ''),
|
| text,
|
| )
|
|
|
|
|
|
|
|
|
| text = re.sub(r"[^\w\s\.\,\?\!\:\;\(\)\-\'\u0600-\u06FF]", ' ', text)
|
|
|
|
|
| text = re.sub(r'\s+', ' ', text).strip()
|
| return text
|
|
|
|
|
|
|
|
|
|
|
| def _split_sentences(text):
|
| """Pecah kalimat di . ! ? (tanda baca-nya tetap menempel pada kalimat)."""
|
| parts = re.split(r'(?<=[.!?])\s+', text)
|
| return [p.strip() for p in parts if p.strip()]
|
|
|
|
|
| def chunk_text(text, target_words=120, overlap_sentences=1):
|
| """
|
| Bagi teks menjadi chunk yang menghormati batas alami (paragraf -> kalimat),
|
| bukan memotong sembarangan di tengah kalimat. Praktik standar RAG modern.
|
|
|
| target_words - target jumlah kata per chunk (chunk bisa sedikit di
|
| atas target karena kalimat terakhir disertakan utuh).
|
| overlap_sentences - berapa kalimat terakhir dari chunk sebelumnya dibawa
|
| ke awal chunk berikutnya (memberi konteks sambungan
|
| supaya jawaban yang menyebrang chunk tetap kelihatan).
|
| """
|
|
|
| paragraphs = [p.strip() for p in re.split(r'\n+', text) if p.strip()]
|
|
|
|
|
| sentences = []
|
| for para in paragraphs:
|
| sentences.extend(_split_sentences(para))
|
|
|
| chunks = []
|
| buf = []
|
| buf_words = 0
|
|
|
| for sent in sentences:
|
| sent_words = len(sent.split())
|
|
|
|
|
|
|
| if sent_words > target_words:
|
| if buf:
|
| chunks.append(" ".join(buf))
|
| buf = buf[-overlap_sentences:] if overlap_sentences else []
|
| buf_words = sum(len(s.split()) for s in buf)
|
| words = sent.split()
|
| for i in range(0, len(words), target_words):
|
| chunks.append(" ".join(words[i:i + target_words]))
|
| buf = []
|
| buf_words = 0
|
| continue
|
|
|
|
|
| if buf_words + sent_words > target_words and buf:
|
| chunks.append(" ".join(buf))
|
|
|
| buf = buf[-overlap_sentences:] if overlap_sentences else []
|
| buf_words = sum(len(s.split()) for s in buf)
|
|
|
| buf.append(sent)
|
| buf_words += sent_words
|
|
|
| if buf:
|
| chunks.append(" ".join(buf))
|
|
|
| return [c for c in chunks if c.strip()]
|
|
|
|
|
|
|
|
|
|
|
|
|
| def clean_json_response(text):
|
| start_idx = text.find('[')
|
| end_idx = text.rfind(']')
|
| if start_idx != -1 and end_idx != -1:
|
| return text[start_idx:end_idx + 1]
|
| return text.strip() |