Coding-With-Bashir commited on
Commit
17437d3
·
verified ·
1 Parent(s): c896f79

Upload .\src\data_collection\data_processor.py with huggingface_hub

Browse files
.//src//data_collection//data_processor.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Data processor for combining and cleaning collected Kinyarwanda data."""
2
+
3
+ import json
4
+ import logging
5
+ import re
6
+ from collections import Counter
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ KINYARWANDACOMMON_WORDS = {
13
+ "ni", "ye", "yo", "ra", "ku", "mu", "gu", "ka", "ke", "ki", "ko",
14
+ "nka", "nk", "icyo", "kandi", "noneho", "ubwo", "icyumweru",
15
+ "uwo", "iyo", "ari", "cyangwa", "kubera", "bitewe", "mu buryo",
16
+ "ubukorikori", "ubuzima", "uburezi", "ubukungu", "politiki",
17
+ "amakuru", "imikino", "imyidagaduro", "ikoranabuhanga",
18
+ "abana", "abantu", "igihugu", "u Rwanda", "Rwandese",
19
+ "ndetse", "kubw", "tw", "mw", "bw", "twegerereje",
20
+ "ibyo", "ubwo", "ari", "yari", "twari", "mwari",
21
+ "ko", "ngo", "ko", "ncy", "nko",
22
+ }
23
+
24
+
25
+ def is_likely_kinyarwanda(text: str) -> bool:
26
+ """Simple heuristic check if text is likely Kinyarwanda.
27
+
28
+ Uses character patterns and common word detection without external packages.
29
+ Not perfect, but catches obvious non-Kinyarwanda text.
30
+ """
31
+ if not text or len(text) < 10:
32
+ return True
33
+
34
+ text_lower = text.lower()
35
+ words = set(text_lower.split())
36
+
37
+ kr_matches = sum(1 for w in KINYARWANDACOMMON_WORDS if w in words)
38
+ if kr_matches >= 2:
39
+ return True
40
+
41
+ latin_chars = sum(1 for c in text if c.isascii() and c.isalpha())
42
+ total_chars = sum(1 for c in text if c.isalpha())
43
+ if total_chars == 0:
44
+ return False
45
+
46
+ latin_ratio = latin_chars / total_chars
47
+ if latin_ratio < 0.85:
48
+ return False
49
+
50
+ arabic_chars = sum(1 for c in text if '\u0600' <= c <= '\u06FF')
51
+ if arabic_chars > total_chars * 0.1:
52
+ return False
53
+
54
+ cjk_chars = sum(1 for c in text if '\u4E00' <= c <= '\u9FFF')
55
+ if cjk_chars > 0:
56
+ return False
57
+
58
+ cyrillic_chars = sum(1 for c in text if '\u0400' <= c <= '\u04FF')
59
+ if cyrillic_chars > total_chars * 0.1:
60
+ return False
61
+
62
+ return True
63
+
64
+
65
+ def count_tokens(text: str) -> int:
66
+ """Count tokens using whitespace splitting (approximate)."""
67
+ return len(text.split())
68
+
69
+
70
+ class DataProcessor:
71
+ """Processes and cleans collected Kinyarwanda data for training."""
72
+
73
+ def __init__(self, raw_dir: str, processed_dir: str):
74
+ self.raw_dir = Path(raw_dir)
75
+ self.processed_dir = Path(processed_dir)
76
+ self.processed_dir.mkdir(parents=True, exist_ok=True)
77
+
78
+ def load_jsonl(self, filepath: Path) -> list[dict[str, Any]]:
79
+ """Load a JSONL file."""
80
+ data = []
81
+ with open(filepath, "r", encoding="utf-8") as f:
82
+ for line in f:
83
+ line = line.strip()
84
+ if line:
85
+ data.append(json.loads(line))
86
+ return data
87
+
88
+ def clean_text(self, text: str) -> str:
89
+ """Clean and normalize Kinyarwanda text."""
90
+ text = re.sub(r"<[^>]+>", "", text)
91
+ text = re.sub(r"http\S+|www\.\S+", "", text)
92
+ text = re.sub(r"\S+@\S+\.\S+", "", text)
93
+ text = re.sub(
94
+ r"[^\w\s.,;:!?'\-\u00C0-\u024F]",
95
+ " ",
96
+ text,
97
+ )
98
+ text = re.sub(r"\s+", " ", text)
99
+ text = text.strip()
100
+
101
+ if len(text) < 5:
102
+ return ""
103
+
104
+ return text
105
+
106
+ def deduplicate(self, texts: list[str]) -> list[str]:
107
+ """Remove duplicate texts, keeping first occurrence."""
108
+ seen = set()
109
+ unique = []
110
+
111
+ for text in texts:
112
+ normalized = text.lower().strip()
113
+ if normalized not in seen:
114
+ seen.add(normalized)
115
+ unique.append(text)
116
+
117
+ return unique
118
+
119
+ def create_training_format(self, texts: list[str]) -> list[dict[str, str]]:
120
+ """Create training format for language modeling."""
121
+ training_data = []
122
+
123
+ for text in texts:
124
+ if text and len(text) > 10:
125
+ training_data.append({"text": text})
126
+
127
+ return training_data
128
+
129
+ def create_instruction_format(self, data: list[dict[str, Any]]) -> list[dict[str, str]]:
130
+ """Create instruction-following format."""
131
+ formatted = []
132
+
133
+ for item in data:
134
+ if "instruction" in item and "output" in item:
135
+ formatted.append({
136
+ "instruction": item["instruction"],
137
+ "input": item.get("input", ""),
138
+ "output": item["output"],
139
+ })
140
+ elif "question" in item and "answer" in item:
141
+ formatted.append({
142
+ "instruction": item["question"],
143
+ "input": "",
144
+ "output": item["answer"],
145
+ })
146
+ elif "prompt" in item and "response" in item:
147
+ formatted.append({
148
+ "instruction": item["prompt"],
149
+ "input": "",
150
+ "output": item["response"],
151
+ })
152
+
153
+ return formatted
154
+
155
+ def create_chat_format(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
156
+ """Create chat/conversation format."""
157
+ formatted = []
158
+
159
+ for item in data:
160
+ if "messages" in item:
161
+ formatted.append({"messages": item["messages"]})
162
+ elif "conversation" in item:
163
+ formatted.append({"messages": item["conversation"]})
164
+ elif "input" in item and "output" in item:
165
+ messages = [
166
+ {"role": "user", "content": item["input"]},
167
+ {"role": "assistant", "content": item["output"]},
168
+ ]
169
+ formatted.append({"messages": messages})
170
+
171
+ return formatted
172
+
173
+ def process_all(self) -> dict[str, Any]:
174
+ """Process all collected data."""
175
+ logger.info("Processing collected data...")
176
+
177
+ all_texts = []
178
+ all_instruction_data = []
179
+ all_chat_data = []
180
+
181
+ file_stats = []
182
+ total_tokens = 0
183
+ total_before = 0
184
+ total_after_lang = 0
185
+ total_after_clean = 0
186
+ total_after_dedup = 0
187
+
188
+ jsonl_files = list(self.raw_dir.glob("*.jsonl"))
189
+ logger.info(f"Found {len(jsonl_files)} JSONL files to process")
190
+
191
+ for jsonl_file in jsonl_files:
192
+ logger.info(f"Processing {jsonl_file.name}...")
193
+
194
+ data = self.load_jsonl(jsonl_file)
195
+ file_rows = len(data)
196
+ total_before += file_rows
197
+
198
+ file_texts = []
199
+ file_instruction = 0
200
+ file_chat = 0
201
+ file_lang_removed = 0
202
+ file_clean_removed = 0
203
+
204
+ for item in data:
205
+ text = self._extract_text(item)
206
+ if text:
207
+ if not is_likely_kinyarwanda(text):
208
+ file_lang_removed += 1
209
+ continue
210
+
211
+ cleaned = self.clean_text(text)
212
+ if cleaned:
213
+ file_texts.append(cleaned)
214
+ total_tokens += count_tokens(cleaned)
215
+ else:
216
+ file_clean_removed += 1
217
+ else:
218
+ file_clean_removed += 1
219
+
220
+ if self._is_instruction(item):
221
+ all_instruction_data.append(item)
222
+ file_instruction += 1
223
+
224
+ if self._is_chat(item):
225
+ all_chat_data.append(item)
226
+ file_chat += 1
227
+
228
+ total_after_lang += len(file_texts) + file_lang_removed
229
+ all_texts.extend(file_texts)
230
+ total_after_clean += len(file_texts)
231
+
232
+ stats = {
233
+ "file": jsonl_file.name,
234
+ "loaded": file_rows,
235
+ "lang_filtered": file_lang_removed,
236
+ "clean_filtered": file_clean_removed,
237
+ "kept": len(file_texts),
238
+ "tokens": sum(count_tokens(t) for t in file_texts),
239
+ "instruction_samples": file_instruction,
240
+ "chat_samples": file_chat,
241
+ }
242
+ file_stats.append(stats)
243
+ logger.info(
244
+ f" {jsonl_file.name}: loaded={file_rows}, "
245
+ f"lang_removed={file_lang_removed}, clean_removed={file_clean_removed}, "
246
+ f"kept={len(file_texts)}"
247
+ )
248
+
249
+ logger.info(f"Total texts before dedup: {len(all_texts)}")
250
+ all_texts = self.deduplicate(all_texts)
251
+ total_after_dedup = len(all_texts)
252
+ logger.info(f"After deduplication: {len(all_texts)} (removed {total_before - len(all_texts)} duplicates)")
253
+
254
+ training_data = self.create_training_format(all_texts)
255
+ training_path = self.processed_dir / "training_data.jsonl"
256
+ with open(training_path, "w", encoding="utf-8") as f:
257
+ for item in training_data:
258
+ f.write(json.dumps(item, ensure_ascii=False) + "\n")
259
+
260
+ instruction_data = self.create_instruction_format(all_instruction_data)
261
+ instruction_path = self.processed_dir / "instruction_data.jsonl"
262
+ with open(instruction_path, "w", encoding="utf-8") as f:
263
+ for item in instruction_data:
264
+ f.write(json.dumps(item, ensure_ascii=False) + "\n")
265
+
266
+ chat_data = self.create_chat_format(all_chat_data)
267
+ chat_path = self.processed_dir / "chat_data.jsonl"
268
+ with open(chat_path, "w", encoding="utf-8") as f:
269
+ for item in chat_data:
270
+ f.write(json.dumps(item, ensure_ascii=False) + "\n")
271
+
272
+ summary = {
273
+ "total_loaded": total_before,
274
+ "total_after_lang_filter": total_after_clean,
275
+ "total_after_dedup": total_after_dedup,
276
+ "total_tokens": total_tokens,
277
+ "total_training_samples": len(training_data),
278
+ "total_instruction": len(instruction_data),
279
+ "total_chat": len(chat_data),
280
+ "per_file_stats": file_stats,
281
+ "output_files": {
282
+ "training": str(training_path),
283
+ "instruction": str(instruction_path),
284
+ "chat": str(chat_path),
285
+ },
286
+ }
287
+
288
+ summary_path = self.processed_dir / "processing_summary.json"
289
+ with open(summary_path, "w", encoding="utf-8") as f:
290
+ json.dump(summary, f, indent=2, ensure_ascii=False)
291
+
292
+ logger.info(f"Processing complete:")
293
+ logger.info(f" Loaded: {total_before:,} rows")
294
+ logger.info(f" After language filter: {total_after_clean:,}")
295
+ logger.info(f" After dedup: {total_after_dedup:,}")
296
+ logger.info(f" Tokens: {total_tokens:,}")
297
+ logger.info(f" Training samples: {len(training_data):,}")
298
+ logger.info(f" Instruction samples: {len(instruction_data):,}")
299
+ logger.info(f" Chat samples: {len(chat_data):,}")
300
+
301
+ return summary
302
+
303
+ def _extract_text(self, item: dict[str, Any]) -> str | None:
304
+ """Extract text from a data item."""
305
+ for key in ["text", "content", "article", "document", "body"]:
306
+ if key in item and isinstance(item[key], str):
307
+ return item[key]
308
+
309
+ for key in item:
310
+ if isinstance(item[key], str) and len(item[key]) > 50:
311
+ return item[key]
312
+
313
+ return None
314
+
315
+ def _is_instruction(self, item: dict[str, Any]) -> bool:
316
+ """Check if item is instruction-following data."""
317
+ instruction_keys = ["instruction", "question", "prompt", "input"]
318
+ return any(key in item for key in instruction_keys)
319
+
320
+ def _is_chat(self, item: dict[str, Any]) -> bool:
321
+ """Check if item is chat/conversation data."""
322
+ chat_keys = ["messages", "conversation", "chat", "dialogue"]
323
+ return any(key in item for key in chat_keys)