goatfahad commited on
Commit
11b7681
·
verified ·
1 Parent(s): 1969622

Upload src\human_condition\nlp\preprocessor.py with huggingface_hub

Browse files
src//human_condition//nlp//preprocessor.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Text preprocessing: cleaning, chunking, and document preparation."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+
6
+ from human_condition.corpus.document import Document
7
+
8
+
9
+ def clean_text(text: str) -> str:
10
+ """Normalize text for downstream NLP.
11
+
12
+ - Lowercase
13
+ - Collapse whitespace runs
14
+ - Strip leading/trailing whitespace
15
+ - Keep sentence-ending punctuation (. ! ?)
16
+ - Replace unicode whitespace variants with ASCII space
17
+ - Remove control characters except newlines
18
+ """
19
+ if not text:
20
+ return ""
21
+ # Replace unicode whitespace variants
22
+ text = re.sub(r"[\u00a0\u2000-\u200b\u202f\u205f\u3000\ufeff]", " ", text)
23
+ # Remove control characters (keep \n)
24
+ text = re.sub(r"[^\S\n]", " ", text)
25
+ text = re.sub(r"\n+", "\n", text)
26
+ text = text.lower().strip()
27
+ # Collapse multiple spaces within lines
28
+ text = re.sub(r"[^\S\n]+", " ", text)
29
+ return text
30
+
31
+
32
+ def _sentence_boundary(text: str, pos: int, max_len: int) -> int:
33
+ """Find the nearest sentence boundary at or before pos."""
34
+ end = min(pos + max_len, len(text))
35
+ # Look for sentence-ending punctuation followed by space or newline
36
+ for i in range(end, max(pos, end - max_len), -1):
37
+ if i > 0 and i < len(text) and text[i - 1] in ".!?" and (
38
+ i >= len(text) or text[i] in (" ", "\n", "\r")
39
+ ):
40
+ return i
41
+ # Fallback: look for any whitespace break
42
+ for i in range(end, max(pos, end - max_len // 2), -1):
43
+ if i > 0 and text[i - 1] in (" ", "\n", "\t"):
44
+ return i
45
+ return end
46
+
47
+
48
+ def chunk_text(
49
+ text: str, max_chunk_size: int = 200, stride: int = 50
50
+ ) -> list[str]:
51
+ """Split text into overlapping chunks.
52
+
53
+ Strategy:
54
+ 1. If text fits in one chunk, return as-is
55
+ 2. Split on sentence boundaries, word boundaries, or character boundary
56
+ 3. stride controls overlap between consecutive chunks
57
+ """
58
+ if not text:
59
+ return []
60
+
61
+ text = text.strip()
62
+ if not text:
63
+ return []
64
+ if len(text) <= max_chunk_size:
65
+ return [text]
66
+
67
+ chunks: list[str] = []
68
+ start = 0
69
+
70
+ while start < len(text):
71
+ end = min(start + max_chunk_size, len(text))
72
+ if end >= len(text):
73
+ chunks.append(text[start:].strip())
74
+ break
75
+
76
+ # Find best split point within [start, end]
77
+ best = _find_sentence_end(text, start, end, max_chunk_size)
78
+ if best <= start + 10:
79
+ best = _find_word_end(text, start, end)
80
+ if best <= start:
81
+ best = end
82
+
83
+ chunk = text[start:best].strip()
84
+ if chunk:
85
+ chunks.append(chunk)
86
+
87
+ # Advance with overlap, ensuring forward progress
88
+ next_start = best - stride
89
+ if next_start <= start:
90
+ next_start = start + 1
91
+ start = next_start
92
+
93
+ # Deduplicate adjacent identical chunks
94
+ deduped: list[str] = []
95
+ for c in chunks:
96
+ if not deduped or c != deduped[-1]:
97
+ deduped.append(c)
98
+
99
+ return deduped
100
+
101
+
102
+ def _find_sentence_end(text: str, start: int, end: int, max_len: int) -> int:
103
+ """Find the closest sentence boundary at or before `end`."""
104
+ search_start = max(end - max_len // 2, start)
105
+ for i in range(end, search_start, -1):
106
+ if i < len(text) - 1 and text[i - 1] in ".!?" and text[i] == " ":
107
+ return i
108
+ return 0
109
+
110
+
111
+ def _find_word_end(text: str, start: int, end: int) -> int:
112
+ """Find the closest word boundary at or before `end`."""
113
+ for i in range(end, start, -1):
114
+ if i < len(text) and text[i] == " ":
115
+ return i
116
+ return 0
117
+
118
+
119
+ def preprocess_documents(
120
+ docs: list[Document], max_chunk_size: int = 200
121
+ ) -> list[Document]:
122
+ """Clean and chunk a list of Documents.
123
+
124
+ Each original document may produce multiple chunked Documents with
125
+ `chunk_N` suffixes on the title.
126
+ """
127
+ result: list[Document] = []
128
+ for doc in docs:
129
+ cleaned = clean_text(doc.text)
130
+ chunks = chunk_text(cleaned, max_chunk_size=max_chunk_size)
131
+ for i, chunk in enumerate(chunks):
132
+ result.append(
133
+ Document(
134
+ source=doc.source,
135
+ title=f"{doc.title} (chunk {i + 1})",
136
+ text=chunk,
137
+ metadata={
138
+ **doc.metadata,
139
+ "chunk_index": i,
140
+ "total_chunks": len(chunks),
141
+ "original_title": doc.title,
142
+ },
143
+ )
144
+ )
145
+ return result