basa 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- basa/__init__.py +33 -0
- basa/augment/__init__.py +0 -0
- basa/augment/noise.py +0 -0
- basa/augment/paraphrase.py +0 -0
- basa/augment/synthetic.py +0 -0
- basa/core/__init__.py +0 -0
- basa/core/normalize.py +240 -0
- basa/core/quick.py +48 -0
- basa/core/slang.py +2071 -0
- basa/core/typo.py +470 -0
- basa/dataset/__init__.py +0 -0
- basa/dataset/builder.py +0 -0
- basa/dataset/cleaner.py +0 -0
- basa/dataset/split.py +0 -0
- basa/dataset/validator.py +0 -0
- basa/evaluate/__init__.py +0 -0
- basa/evaluate/factual.py +0 -0
- basa/evaluate/metrics.py +0 -0
- basa/evaluate/similarity.py +0 -0
- basa/tokenize/__init__.py +0 -0
- basa/tokenize/lang_detect.py +0 -0
- basa/tokenize/sentence.py +0 -0
- basa/tokenize/word.py +0 -0
- basa/translate/__init__.py +0 -0
- basa/translate/jv_id.py +0 -0
- basa/translate/router.py +0 -0
- basa/translate/su_id.py +0 -0
- basa/utils/__init__.py +0 -0
- basa/utils/constants.py +0 -0
- basa/utils/regex.py +0 -0
- basa/utils/text_clean.py +0 -0
- basa-0.1.0.dist-info/METADATA +439 -0
- basa-0.1.0.dist-info/RECORD +35 -0
- basa-0.1.0.dist-info/WHEEL +4 -0
- basa-0.1.0.dist-info/licenses/LICENSE +21 -0
basa/core/typo.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BASA - Typo Correction Module
|
|
3
|
+
==============================
|
|
4
|
+
Detects and corrects out-of-vocabulary words using Levenshtein distance
|
|
5
|
+
against a user-supplied vocabulary set.
|
|
6
|
+
|
|
7
|
+
Design philosophy:
|
|
8
|
+
- Vocabulary is NOT bundled into the library core. Every domain has
|
|
9
|
+
different vocabulary needs (fintech, e-commerce, ML, telecom), so
|
|
10
|
+
the caller is responsible for providing a relevant word list.
|
|
11
|
+
- The corrector is intentionally conservative: short words and
|
|
12
|
+
low-confidence matches are left untouched to avoid false positives.
|
|
13
|
+
- A lookup cache prevents redundant computation when the same token
|
|
14
|
+
appears many times in a corpus (very common in real-world data).
|
|
15
|
+
|
|
16
|
+
Limitations (to be addressed in v0.2):
|
|
17
|
+
- Linear scan over vocab is O(|vocab| × |word| × |candidate|).
|
|
18
|
+
For large vocabularies (50 k+ words) consider replacing the inner
|
|
19
|
+
loop with a BK-Tree or SymSpell index.
|
|
20
|
+
- Correction is purely character-level; no phonetic or semantic context
|
|
21
|
+
is used. The closest-distance word wins regardless of meaning.
|
|
22
|
+
|
|
23
|
+
Pipeline position:
|
|
24
|
+
Typo correction should run AFTER slang normalization and repeated-
|
|
25
|
+
character reduction, so that tokens like "gkkkk" are already collapsed
|
|
26
|
+
to "gk" (and then translated to "tidak") before the corrector sees them.
|
|
27
|
+
|
|
28
|
+
Recommended order:
|
|
29
|
+
lowercase → repeated-char normalization → slang replacement
|
|
30
|
+
→ typo correction → whitespace cleanup
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
from basa.core.typo import TypoCorrector
|
|
34
|
+
|
|
35
|
+
corrector = TypoCorrector(min_word_length=4)
|
|
36
|
+
corrector.add_to_vocab({"makan", "minum", "masak", "mander"})
|
|
37
|
+
|
|
38
|
+
corrector.correct("mander") # → "mander" (in vocab, no change)
|
|
39
|
+
corrector.correct("mnder") # → "mander" (distance 1, corrected)
|
|
40
|
+
corrector.correct("ok") # → "ok" (below min_word_length, skipped)
|
|
41
|
+
|
|
42
|
+
# Batch sentence correction
|
|
43
|
+
corrector.correct_text("saya mkan dan mnder")
|
|
44
|
+
# → "saya makan dan mander"
|
|
45
|
+
|
|
46
|
+
# Inspect cache hit rate (useful for profiling)
|
|
47
|
+
print(corrector.cache_info())
|
|
48
|
+
|
|
49
|
+
Notes:
|
|
50
|
+
- All comparisons are lowercased internally.
|
|
51
|
+
- The module-level singleton `typo` ships with an empty vocabulary.
|
|
52
|
+
Add domain words via add_to_vocab() or pass vocab= at construction.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
# LEVENSHTEIN DISTANCE
|
|
59
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def levenshtein_distance(s1: str, s2: str) -> int:
|
|
63
|
+
"""
|
|
64
|
+
Compute the Levenshtein (edit) distance between two strings.
|
|
65
|
+
|
|
66
|
+
The distance counts the minimum number of single-character edits
|
|
67
|
+
(insertions, deletions, or substitutions) required to transform
|
|
68
|
+
s1 into s2.
|
|
69
|
+
|
|
70
|
+
Implementation uses the standard iterative DP approach with a
|
|
71
|
+
single previous-row buffer, keeping memory usage O(min(|s1|, |s2|)).
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
s1: First string.
|
|
75
|
+
s2: Second string.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Non-negative integer representing the edit distance.
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
>>> levenshtein_distance("makan", "makan")
|
|
82
|
+
0
|
|
83
|
+
>>> levenshtein_distance("mkan", "makan")
|
|
84
|
+
1
|
|
85
|
+
>>> levenshtein_distance("mnder", "mander")
|
|
86
|
+
1
|
|
87
|
+
>>> levenshtein_distance("xyz", "abc")
|
|
88
|
+
3
|
|
89
|
+
"""
|
|
90
|
+
# Ensure s1 is the longer string (optimises the inner loop slightly)
|
|
91
|
+
if len(s1) < len(s2):
|
|
92
|
+
return levenshtein_distance(s2, s1)
|
|
93
|
+
|
|
94
|
+
# Edge case: one string is empty
|
|
95
|
+
if len(s2) == 0:
|
|
96
|
+
return len(s1)
|
|
97
|
+
|
|
98
|
+
previous_row = range(len(s2) + 1)
|
|
99
|
+
|
|
100
|
+
for i, c1 in enumerate(s1):
|
|
101
|
+
current_row = [i + 1]
|
|
102
|
+
for j, c2 in enumerate(s2):
|
|
103
|
+
insertions = previous_row[j + 1] + 1
|
|
104
|
+
deletions = current_row[j] + 1
|
|
105
|
+
substitutions = previous_row[j] + (c1 != c2)
|
|
106
|
+
current_row.append(min(insertions, deletions, substitutions))
|
|
107
|
+
previous_row = current_row
|
|
108
|
+
|
|
109
|
+
return previous_row[-1]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
+
# TYPO CORRECTOR
|
|
114
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TypoCorrector:
|
|
118
|
+
"""
|
|
119
|
+
Context-free typo corrector based on minimum Levenshtein distance.
|
|
120
|
+
|
|
121
|
+
The corrector scans an unknown token against a vocabulary set and
|
|
122
|
+
returns the closest-matching word within a configurable edit distance.
|
|
123
|
+
Two safety guards prevent aggressive over-correction:
|
|
124
|
+
|
|
125
|
+
1. ``min_word_length``: tokens shorter than this threshold are returned
|
|
126
|
+
unchanged. Very short words (1–3 chars) have many near-neighbours
|
|
127
|
+
at distance 1–2, making false positives very likely.
|
|
128
|
+
|
|
129
|
+
2. ``min_confidence``: a soft ratio guard. After the best candidate
|
|
130
|
+
is found, confidence is computed as::
|
|
131
|
+
|
|
132
|
+
confidence = 1 - (edit_distance / len(word))
|
|
133
|
+
|
|
134
|
+
If confidence is below this threshold the original word is kept.
|
|
135
|
+
Default 0.5 means at most half the characters may be wrong.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
vocab: Initial vocabulary set (lowercase recommended).
|
|
139
|
+
min_word_length: Minimum token length to attempt correction.
|
|
140
|
+
Tokens below this length are returned as-is.
|
|
141
|
+
Default: 4.
|
|
142
|
+
min_confidence: Minimum correction confidence in [0, 1].
|
|
143
|
+
Default: 0.5.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> corrector = TypoCorrector(min_word_length=4)
|
|
147
|
+
>>> corrector.add_to_vocab({"makan", "minum", "masak"})
|
|
148
|
+
>>> corrector.correct("mkan")
|
|
149
|
+
'makan'
|
|
150
|
+
>>> corrector.correct("ok") # length 2 < min_word_length
|
|
151
|
+
'ok'
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
vocab: set[str] | None = None,
|
|
157
|
+
min_word_length: int = 4,
|
|
158
|
+
min_confidence: float = 0.5,
|
|
159
|
+
) -> None:
|
|
160
|
+
self.vocab: set[str] = {w.lower() for w in vocab} if vocab else set()
|
|
161
|
+
self.min_word_length = min_word_length
|
|
162
|
+
self.min_confidence = min_confidence
|
|
163
|
+
|
|
164
|
+
# ── Protected vocab (whitelist) ───────────────────────────────────────
|
|
165
|
+
# Words in this set are always returned unchanged by correct(), even
|
|
166
|
+
# when they are not in self.vocab. This lets normalize.py silently
|
|
167
|
+
# shield all slang-output values (e.g. "mahal", "terima kasih") from
|
|
168
|
+
# being mis-corrected back to a short user vocab like {"nasi", "goreng"}.
|
|
169
|
+
# Managed internally — not exposed as a public vocabulary to the caller.
|
|
170
|
+
self._protected: set[str] = set()
|
|
171
|
+
|
|
172
|
+
# ── Lookup cache ─────────────────────────────────────────────────────
|
|
173
|
+
# Maps lowercased token → corrected word.
|
|
174
|
+
# In real-world corpora the same misspelling often appears hundreds of
|
|
175
|
+
# times (e.g. "gk", "bgt", "udh"). Caching avoids recomputing the
|
|
176
|
+
# full vocab scan on every occurrence.
|
|
177
|
+
# The cache is invalidated whenever the vocabulary changes.
|
|
178
|
+
self._cache: dict[str, str] = {}
|
|
179
|
+
|
|
180
|
+
# ── Cache statistics ─────────────────────────────────────────────────
|
|
181
|
+
# Useful for profiling and tuning cache effectiveness.
|
|
182
|
+
self._cache_hits: int = 0
|
|
183
|
+
self._cache_misses: int = 0
|
|
184
|
+
|
|
185
|
+
# ─── Internal ────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def _invalidate_cache(self) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Clear the lookup cache.
|
|
190
|
+
|
|
191
|
+
Must be called whenever self.vocab is mutated so that stale
|
|
192
|
+
corrections are not returned for newly added words.
|
|
193
|
+
"""
|
|
194
|
+
self._cache.clear()
|
|
195
|
+
self._cache_hits = 0
|
|
196
|
+
self._cache_misses = 0
|
|
197
|
+
|
|
198
|
+
# ─── Protected vocab management ──────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
def set_protected(self, words: set[str]) -> None:
|
|
201
|
+
"""
|
|
202
|
+
Replace the internal whitelist with the given word set.
|
|
203
|
+
|
|
204
|
+
Words in the protected set are always returned as-is by correct(),
|
|
205
|
+
bypassing both the Levenshtein scan and the confidence filter.
|
|
206
|
+
This is used by normalize.py to shield slang-output values from
|
|
207
|
+
accidental re-correction.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
words: Set of lowercase words to protect (multi-word values are
|
|
211
|
+
split on whitespace so each token is protected individually).
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
>>> corrector.set_protected({"mahal", "terima kasih"})
|
|
215
|
+
"""
|
|
216
|
+
self._protected = {
|
|
217
|
+
token for phrase in words for token in phrase.lower().split()
|
|
218
|
+
}
|
|
219
|
+
self._invalidate_cache()
|
|
220
|
+
|
|
221
|
+
# ─── Vocabulary management ───────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
def add_to_vocab(self, words: set[str]) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Add words to the vocabulary and invalidate the lookup cache.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
words: Set of words to add (stored as lowercase).
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
>>> corrector.add_to_vocab({"lightgbm", "catboost", "xgboost"})
|
|
232
|
+
"""
|
|
233
|
+
self.vocab.update(w.lower() for w in words)
|
|
234
|
+
self._invalidate_cache()
|
|
235
|
+
|
|
236
|
+
def remove_from_vocab(self, words: set[str]) -> None:
|
|
237
|
+
"""
|
|
238
|
+
Remove words from the vocabulary and invalidate the lookup cache.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
words: Set of words to remove.
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
>>> corrector.remove_from_vocab({"kredivo"})
|
|
245
|
+
"""
|
|
246
|
+
self.vocab -= {w.lower() for w in words}
|
|
247
|
+
self._invalidate_cache()
|
|
248
|
+
|
|
249
|
+
def clear_vocab(self) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Empty the vocabulary entirely and invalidate the cache.
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
>>> corrector.clear_vocab()
|
|
255
|
+
>>> len(corrector)
|
|
256
|
+
0
|
|
257
|
+
"""
|
|
258
|
+
self.vocab.clear()
|
|
259
|
+
self._invalidate_cache()
|
|
260
|
+
|
|
261
|
+
# ─── Correction ──────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
def correct(self, word: str, max_dist: int = 2) -> str:
|
|
264
|
+
"""
|
|
265
|
+
Correct a single token against the vocabulary.
|
|
266
|
+
|
|
267
|
+
Decision logic:
|
|
268
|
+
1. If vocab is empty → return original (nothing to compare).
|
|
269
|
+
2. If token is already in vocab → return original (correct).
|
|
270
|
+
3. If len(token) < min_word_length → return original (too risky).
|
|
271
|
+
4. Check cache → return cached result if available.
|
|
272
|
+
5. Linear scan over vocab with length-delta pruning.
|
|
273
|
+
6. Apply confidence filter → return original if below threshold.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
word: Input token (any case).
|
|
277
|
+
max_dist: Maximum Levenshtein distance to consider a correction.
|
|
278
|
+
Higher values are more permissive but riskier.
|
|
279
|
+
Default: 2.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Corrected word (lowercase) if a confident match is found,
|
|
283
|
+
otherwise the original word unchanged.
|
|
284
|
+
|
|
285
|
+
Examples:
|
|
286
|
+
>>> corrector.correct("mkan") # dist 1 from "makan"
|
|
287
|
+
'makan'
|
|
288
|
+
>>> corrector.correct("ok") # below min_word_length
|
|
289
|
+
'ok'
|
|
290
|
+
>>> corrector.correct("xyz123") # no close match in vocab
|
|
291
|
+
'xyz123'
|
|
292
|
+
"""
|
|
293
|
+
word_lower = word.lower()
|
|
294
|
+
|
|
295
|
+
# Guard 1: No vocab loaded — nothing to do
|
|
296
|
+
if not self.vocab:
|
|
297
|
+
return word
|
|
298
|
+
|
|
299
|
+
# Guard 2: Word is protected (slang output whitelist) — skip correction
|
|
300
|
+
# This prevents words like "mahal" or "kasih" (produced by slang
|
|
301
|
+
# normalization) from being mis-corrected to a short user vocab word
|
|
302
|
+
# (e.g. "nasi" or "goreng") that happens to be close in edit distance.
|
|
303
|
+
if word_lower in self._protected:
|
|
304
|
+
return word
|
|
305
|
+
|
|
306
|
+
# Guard 3: Word already in vocab — already correct
|
|
307
|
+
if word_lower in self.vocab:
|
|
308
|
+
return word
|
|
309
|
+
|
|
310
|
+
# Guard 4: Word too short — correction is too risky
|
|
311
|
+
# e.g. "di", "ya", "ok" have many neighbours at distance 1–2
|
|
312
|
+
if len(word_lower) < self.min_word_length:
|
|
313
|
+
return word
|
|
314
|
+
|
|
315
|
+
# Guard 5: Return cached result if available
|
|
316
|
+
if word_lower in self._cache:
|
|
317
|
+
self._cache_hits += 1
|
|
318
|
+
return self._cache[word_lower]
|
|
319
|
+
|
|
320
|
+
self._cache_misses += 1
|
|
321
|
+
|
|
322
|
+
# ── Linear scan with length-delta pruning ────────────────────────────
|
|
323
|
+
# Skipping candidates whose length differs by more than max_dist
|
|
324
|
+
# avoids computing Levenshtein for obviously too-different words,
|
|
325
|
+
# providing a significant speedup for large vocabularies.
|
|
326
|
+
best_match = word
|
|
327
|
+
min_dist = float("inf")
|
|
328
|
+
|
|
329
|
+
for candidate in self.vocab:
|
|
330
|
+
# Fast pre-filter: if lengths differ by more than max_dist,
|
|
331
|
+
# the edit distance is guaranteed to exceed max_dist — skip.
|
|
332
|
+
if abs(len(word_lower) - len(candidate)) > max_dist:
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
dist = levenshtein_distance(word_lower, candidate)
|
|
336
|
+
|
|
337
|
+
if dist < min_dist and dist <= max_dist:
|
|
338
|
+
min_dist = dist
|
|
339
|
+
best_match = candidate
|
|
340
|
+
|
|
341
|
+
# ── Confidence filter ────────────────────────────────────────────────
|
|
342
|
+
# confidence = fraction of characters that are "correct".
|
|
343
|
+
# e.g. "mkan" (len 4) corrected to "makan" (dist 1):
|
|
344
|
+
# confidence = 1 - 1/4 = 0.75 → accepted if min_confidence ≤ 0.75
|
|
345
|
+
if best_match != word:
|
|
346
|
+
confidence = 1.0 - (min_dist / max(len(word_lower), 1))
|
|
347
|
+
if confidence < self.min_confidence:
|
|
348
|
+
best_match = word # Not confident enough — keep original
|
|
349
|
+
|
|
350
|
+
# Store result in cache before returning
|
|
351
|
+
self._cache[word_lower] = best_match
|
|
352
|
+
return best_match
|
|
353
|
+
|
|
354
|
+
def correct_text(self, text: str, max_dist: int = 2) -> str:
|
|
355
|
+
"""
|
|
356
|
+
Correct all tokens in a text string.
|
|
357
|
+
|
|
358
|
+
Splits on whitespace, corrects each token independently, then
|
|
359
|
+
rejoins with a single space. Punctuation attached to words
|
|
360
|
+
(e.g. "mkan,") is treated as part of the token and may not
|
|
361
|
+
match the vocab — consider stripping punctuation upstream.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
text: Input text string.
|
|
365
|
+
max_dist: Passed through to correct().
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Text with out-of-vocab tokens corrected where possible.
|
|
369
|
+
|
|
370
|
+
Example:
|
|
371
|
+
>>> corrector.correct_text("saya mkan dan mnder sore ini")
|
|
372
|
+
'saya makan dan mander sore ini'
|
|
373
|
+
"""
|
|
374
|
+
if not text:
|
|
375
|
+
return text
|
|
376
|
+
|
|
377
|
+
corrected = [self.correct(w, max_dist) for w in text.split()]
|
|
378
|
+
return " ".join(corrected)
|
|
379
|
+
|
|
380
|
+
def suggest(self, word: str, max_dist: int = 2, top_k: int = 3) -> list[str]:
|
|
381
|
+
"""
|
|
382
|
+
Suggest closest vocabulary matches for an unknown word.
|
|
383
|
+
|
|
384
|
+
Unlike correct(), this returns a list of possibilities without
|
|
385
|
+
forcing a replacement, making it safe for exploratory pipelines.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
word: Input token (any case).
|
|
389
|
+
max_dist: Maximum Levenshtein distance to consider.
|
|
390
|
+
top_k: Maximum number of suggestions to return.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
List of candidate strings, sorted by closest edit distance.
|
|
394
|
+
"""
|
|
395
|
+
word_lower = word.lower()
|
|
396
|
+
|
|
397
|
+
if not self.vocab:
|
|
398
|
+
return []
|
|
399
|
+
|
|
400
|
+
if word_lower in self.vocab:
|
|
401
|
+
return [word_lower]
|
|
402
|
+
|
|
403
|
+
candidates = []
|
|
404
|
+
for candidate in self.vocab:
|
|
405
|
+
if abs(len(word_lower) - len(candidate)) > max_dist:
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
dist = levenshtein_distance(word_lower, candidate)
|
|
409
|
+
if dist <= max_dist:
|
|
410
|
+
candidates.append((dist, candidate))
|
|
411
|
+
|
|
412
|
+
candidates.sort(key=lambda x: (x[0], x[1]))
|
|
413
|
+
return [c[1] for c in candidates[:top_k]]
|
|
414
|
+
|
|
415
|
+
# ─── Diagnostics ─────────────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
def cache_info(self) -> dict[str, int]:
|
|
418
|
+
"""
|
|
419
|
+
Return cache hit/miss statistics.
|
|
420
|
+
|
|
421
|
+
Useful for profiling: a high hit rate indicates the cache is
|
|
422
|
+
working well; a low rate may suggest highly varied input.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Dict with keys ``hits``, ``misses``, and ``size``.
|
|
426
|
+
|
|
427
|
+
Example:
|
|
428
|
+
>>> corrector.cache_info()
|
|
429
|
+
{'hits': 120, 'misses': 35, 'size': 35}
|
|
430
|
+
"""
|
|
431
|
+
return {
|
|
432
|
+
"hits": self._cache_hits,
|
|
433
|
+
"misses": self._cache_misses,
|
|
434
|
+
"size": len(self._cache),
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# ─── Dunder helpers ──────────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
def __len__(self) -> int:
|
|
440
|
+
"""Return the number of words in the vocabulary."""
|
|
441
|
+
return len(self.vocab)
|
|
442
|
+
|
|
443
|
+
def __contains__(self, word: str) -> bool:
|
|
444
|
+
"""Check if a word is in the vocabulary (case-insensitive)."""
|
|
445
|
+
return word.lower() in self.vocab
|
|
446
|
+
|
|
447
|
+
def __repr__(self) -> str:
|
|
448
|
+
return (
|
|
449
|
+
f"TypoCorrector("
|
|
450
|
+
f"vocab_size={len(self.vocab)}, "
|
|
451
|
+
f"min_word_length={self.min_word_length}, "
|
|
452
|
+
f"min_confidence={self.min_confidence})"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
457
|
+
# MODULE-LEVEL CONVENIENCE SINGLETON
|
|
458
|
+
#
|
|
459
|
+
# Ships with an empty vocabulary — the caller must populate it.
|
|
460
|
+
#
|
|
461
|
+
# Quick start:
|
|
462
|
+
# from basa.core.typo import typo
|
|
463
|
+
# typo.add_to_vocab({"makan", "minum", "masak"})
|
|
464
|
+
# typo.correct_text("saya mkan dan mnder")
|
|
465
|
+
#
|
|
466
|
+
# For domain-specific use:
|
|
467
|
+
# from basa.core.typo import TypoCorrector
|
|
468
|
+
# corrector = TypoCorrector(vocab=my_word_set, min_word_length=4)
|
|
469
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
470
|
+
typo = TypoCorrector()
|
basa/dataset/__init__.py
ADDED
|
File without changes
|
basa/dataset/builder.py
ADDED
|
File without changes
|
basa/dataset/cleaner.py
ADDED
|
File without changes
|
basa/dataset/split.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
basa/evaluate/factual.py
ADDED
|
File without changes
|
basa/evaluate/metrics.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
basa/tokenize/word.py
ADDED
|
File without changes
|
|
File without changes
|
basa/translate/jv_id.py
ADDED
|
File without changes
|
basa/translate/router.py
ADDED
|
File without changes
|
basa/translate/su_id.py
ADDED
|
File without changes
|
basa/utils/__init__.py
ADDED
|
File without changes
|
basa/utils/constants.py
ADDED
|
File without changes
|
basa/utils/regex.py
ADDED
|
File without changes
|
basa/utils/text_clean.py
ADDED
|
File without changes
|