BeSok-app / py /text_utils.py
IndahPurn's picture
Upload 29 files
5b8ef5f verified
Raw
History Blame Contribute Delete
4.11 kB
import re
def normalize_pdf_text(text):
"""Pembersih teks hasil ekstraksi PDF."""
# 1) Satukan rentetan huruf tunggal yang dipisah spasi (artefak PDF
# yang sering memecah kata jadi "s h o l a t"). Hanya gabungkan
# kalau ada >=3 huruf tunggal beruntun -> mencegah salah-gabung
# kasus sah seperti "Andi a man" jadi "Andia man".
text = re.sub(
r'\b([a-zA-Z](?:\s[a-zA-Z]){2,})\b',
lambda m: m.group(1).replace(' ', ''),
text,
)
# 2) Buang karakter yang bukan huruf/angka/spasi/tanda baca umum.
# APOSTROF (') dipertahankan -> penting untuk transliterasi istilah
# Islam: a'la, i'tidal, ta'awudz, dll. Rentang Arab juga utuh.
text = re.sub(r"[^\w\s\.\,\?\!\:\;\(\)\-\'\u0600-\u06FF]", ' ', text)
# 3) Ratakan spasi berlebih jadi satu spasi.
text = re.sub(r'\s+', ' ', text).strip()
return text
# ====================================================================
# CHUNKING - recursive, hormati batas paragraf & kalimat
# ====================================================================
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).
"""
# Pecah dulu per paragraf (newline tunggal pun dianggap pemisah).
paragraphs = [p.strip() for p in re.split(r'\n+', text) if p.strip()]
# Lalu pecah paragraf jadi kalimat-kalimat.
sentences = []
for para in paragraphs:
sentences.extend(_split_sentences(para))
chunks = []
buf = [] # kalimat-kalimat yang sedang dikumpulkan
buf_words = 0
for sent in sentences:
sent_words = len(sent.split())
# Edge case: kalimat tunggal yang panjangnya jauh di atas target.
# Tutup buffer sekarang, lalu paksa pecah kalimat itu per kata.
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
# Kalau menambah kalimat ini bakal lewat target -> tutup chunk dulu.
if buf_words + sent_words > target_words and buf:
chunks.append(" ".join(buf))
# Overlap: bawa N kalimat terakhir ke chunk berikutnya.
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()]
# ====================================================================
# LEGACY - tidak dipakai lagi setelah rag_engine pakai structured output.
# Dipertahankan untuk kompatibilitas mundur (kalau ada modul lain yg impor).
# ====================================================================
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()