fastlangml 1.0.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.
- fastlangml/__init__.py +164 -0
- fastlangml/backends/__init__.py +283 -0
- fastlangml/backends/base.py +73 -0
- fastlangml/backends/fasttext_backend.py +96 -0
- fastlangml/backends/langdetect_backend.py +90 -0
- fastlangml/backends/langid_backend.py +103 -0
- fastlangml/backends/lingua_backend.py +145 -0
- fastlangml/backends/pycld3_backend.py +101 -0
- fastlangml/cache.py +86 -0
- fastlangml/cli/__init__.py +5 -0
- fastlangml/cli/main.py +522 -0
- fastlangml/codeswitching.py +350 -0
- fastlangml/config/__init__.py +1 -0
- fastlangml/context/__init__.py +8 -0
- fastlangml/context/conversation.py +216 -0
- fastlangml/detector.py +900 -0
- fastlangml/ensemble/__init__.py +19 -0
- fastlangml/ensemble/confusion.py +301 -0
- fastlangml/ensemble/voting.py +362 -0
- fastlangml/exceptions.py +49 -0
- fastlangml/hints/__init__.py +9 -0
- fastlangml/hints/dictionary.py +305 -0
- fastlangml/hints/persistence.py +172 -0
- fastlangml/normalize.py +289 -0
- fastlangml/preprocessing/__init__.py +11 -0
- fastlangml/preprocessing/name_filter.py +84 -0
- fastlangml/preprocessing/proper_noun_filter.py +201 -0
- fastlangml/preprocessing/script_filter.py +272 -0
- fastlangml/py.typed +0 -0
- fastlangml/result.py +127 -0
- fastlangml-1.0.0.dist-info/METADATA +879 -0
- fastlangml-1.0.0.dist-info/RECORD +35 -0
- fastlangml-1.0.0.dist-info/WHEEL +4 -0
- fastlangml-1.0.0.dist-info/entry_points.txt +2 -0
- fastlangml-1.0.0.dist-info/licenses/LICENSE +21 -0
fastlangml/__init__.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""FastLangID - Fast, accurate language detection with multiple backends.
|
|
2
|
+
|
|
3
|
+
A production-first language detection toolkit optimized for short text that provides:
|
|
4
|
+
- Stable, deterministic API
|
|
5
|
+
- Unknown/abstain support (returns 'und' with reason when uncertain)
|
|
6
|
+
- Top-k predictions with consistent confidence semantics
|
|
7
|
+
- Multi-backend ensembling (fasttext, langdetect, lingua, pycld3, langid)
|
|
8
|
+
- Batch + caching for throughput
|
|
9
|
+
- Benchmark harness to compare backends
|
|
10
|
+
|
|
11
|
+
Quick Start:
|
|
12
|
+
>>> from fastlangml import detect
|
|
13
|
+
>>> result = detect("Bonjour le monde")
|
|
14
|
+
>>> result.lang
|
|
15
|
+
'fr'
|
|
16
|
+
>>> result.confidence
|
|
17
|
+
0.95
|
|
18
|
+
|
|
19
|
+
>>> from fastlangml import detect
|
|
20
|
+
>>> result = detect("Hello world", top_k=3)
|
|
21
|
+
>>> result.candidates
|
|
22
|
+
[Candidate(lang='en', confidence=0.95), ...]
|
|
23
|
+
|
|
24
|
+
Ensemble Usage:
|
|
25
|
+
>>> from fastlangml import ensemble
|
|
26
|
+
>>> result = ensemble(
|
|
27
|
+
... "Bonjour",
|
|
28
|
+
... backends=["fasttext", "lingua"],
|
|
29
|
+
... weights={"fasttext": 0.7, "lingua": 0.3},
|
|
30
|
+
... )
|
|
31
|
+
>>> result.lang
|
|
32
|
+
'fr'
|
|
33
|
+
|
|
34
|
+
Advanced Usage:
|
|
35
|
+
>>> from fastlangml import FastLangDetector
|
|
36
|
+
>>> detector = FastLangDetector()
|
|
37
|
+
>>> detector.add_hint("merci", "fr")
|
|
38
|
+
>>> result = detector.detect("Merci beaucoup!")
|
|
39
|
+
>>> result.lang
|
|
40
|
+
'fr'
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
from fastlangml.backends import (
|
|
46
|
+
Backend,
|
|
47
|
+
backend,
|
|
48
|
+
get_available_backends,
|
|
49
|
+
list_registered_backends,
|
|
50
|
+
register_backend,
|
|
51
|
+
unregister_backend,
|
|
52
|
+
)
|
|
53
|
+
from fastlangml.codeswitching import (
|
|
54
|
+
CodeSwitchDetector,
|
|
55
|
+
CodeSwitchResult,
|
|
56
|
+
detect_code_switching_pattern,
|
|
57
|
+
)
|
|
58
|
+
from fastlangml.context.conversation import ConversationContext, ConversationTurn
|
|
59
|
+
from fastlangml.detector import (
|
|
60
|
+
DetectionConfig,
|
|
61
|
+
FastLangDetector,
|
|
62
|
+
FastLangDetectorBuilder,
|
|
63
|
+
detect,
|
|
64
|
+
detect_batch,
|
|
65
|
+
ensemble,
|
|
66
|
+
)
|
|
67
|
+
from fastlangml.ensemble.confusion import (
|
|
68
|
+
ConfusionResolver,
|
|
69
|
+
LanguageSimilarity,
|
|
70
|
+
)
|
|
71
|
+
from fastlangml.ensemble.voting import (
|
|
72
|
+
ConsensusVoting,
|
|
73
|
+
HardVoting,
|
|
74
|
+
SoftVoting,
|
|
75
|
+
VotingStrategy,
|
|
76
|
+
WeightedVoting,
|
|
77
|
+
)
|
|
78
|
+
from fastlangml.exceptions import (
|
|
79
|
+
BackendError,
|
|
80
|
+
BackendUnavailableError,
|
|
81
|
+
ConfigurationError,
|
|
82
|
+
DetectionError,
|
|
83
|
+
FastLangError,
|
|
84
|
+
HintError,
|
|
85
|
+
NoBackendsAvailableError,
|
|
86
|
+
)
|
|
87
|
+
from fastlangml.hints.dictionary import HintDictionary
|
|
88
|
+
from fastlangml.hints.persistence import HintPersistence
|
|
89
|
+
from fastlangml.normalize import (
|
|
90
|
+
compute_text_stats,
|
|
91
|
+
normalize_lang_tag,
|
|
92
|
+
normalize_text,
|
|
93
|
+
)
|
|
94
|
+
from fastlangml.preprocessing.script_filter import Script, detect_script
|
|
95
|
+
from fastlangml.result import (
|
|
96
|
+
Candidate,
|
|
97
|
+
DetectionResult,
|
|
98
|
+
Reasons,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
from importlib.metadata import version as _get_version
|
|
103
|
+
__version__ = _get_version("fastlangml")
|
|
104
|
+
except Exception:
|
|
105
|
+
__version__ = "0.1.0" # Fallback for development
|
|
106
|
+
|
|
107
|
+
__all__ = [
|
|
108
|
+
# Version
|
|
109
|
+
"__version__",
|
|
110
|
+
# Main API
|
|
111
|
+
"detect",
|
|
112
|
+
"detect_batch",
|
|
113
|
+
"ensemble",
|
|
114
|
+
# Result types
|
|
115
|
+
"DetectionResult",
|
|
116
|
+
"Candidate",
|
|
117
|
+
"Reasons",
|
|
118
|
+
# Detector
|
|
119
|
+
"FastLangDetector",
|
|
120
|
+
"FastLangDetectorBuilder",
|
|
121
|
+
"DetectionConfig",
|
|
122
|
+
# Context
|
|
123
|
+
"ConversationContext",
|
|
124
|
+
"ConversationTurn",
|
|
125
|
+
# Hints
|
|
126
|
+
"HintDictionary",
|
|
127
|
+
"HintPersistence",
|
|
128
|
+
# Script detection
|
|
129
|
+
"Script",
|
|
130
|
+
"detect_script",
|
|
131
|
+
# Normalization
|
|
132
|
+
"compute_text_stats",
|
|
133
|
+
"normalize_text",
|
|
134
|
+
"normalize_lang_tag",
|
|
135
|
+
# Utilities
|
|
136
|
+
"get_available_backends",
|
|
137
|
+
# Custom backend registration
|
|
138
|
+
"backend", # Decorator (preferred)
|
|
139
|
+
"register_backend",
|
|
140
|
+
"unregister_backend",
|
|
141
|
+
"list_registered_backends",
|
|
142
|
+
"Backend",
|
|
143
|
+
# Voting strategies (for customization)
|
|
144
|
+
"VotingStrategy",
|
|
145
|
+
"HardVoting",
|
|
146
|
+
"SoftVoting",
|
|
147
|
+
"WeightedVoting",
|
|
148
|
+
"ConsensusVoting",
|
|
149
|
+
# Exceptions
|
|
150
|
+
"FastLangError",
|
|
151
|
+
"DetectionError",
|
|
152
|
+
"BackendUnavailableError",
|
|
153
|
+
"BackendError",
|
|
154
|
+
"ConfigurationError",
|
|
155
|
+
"HintError",
|
|
156
|
+
"NoBackendsAvailableError",
|
|
157
|
+
# Confusion resolution
|
|
158
|
+
"ConfusionResolver",
|
|
159
|
+
"LanguageSimilarity",
|
|
160
|
+
# Code-switching detection
|
|
161
|
+
"CodeSwitchDetector",
|
|
162
|
+
"CodeSwitchResult",
|
|
163
|
+
"detect_code_switching_pattern",
|
|
164
|
+
]
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Backend implementations for language detection.
|
|
2
|
+
|
|
3
|
+
Supports built-in backends and custom backends via decorator registration.
|
|
4
|
+
|
|
5
|
+
Example using decorator:
|
|
6
|
+
>>> from fastlangml import backend, Backend
|
|
7
|
+
>>> from fastlangml.backends.base import DetectionResult
|
|
8
|
+
>>>
|
|
9
|
+
>>> @backend("mybackend", reliability=4)
|
|
10
|
+
... class MyBackend(Backend):
|
|
11
|
+
... @property
|
|
12
|
+
... def name(self) -> str:
|
|
13
|
+
... return "mybackend"
|
|
14
|
+
...
|
|
15
|
+
... @property
|
|
16
|
+
... def is_available(self) -> bool:
|
|
17
|
+
... return True
|
|
18
|
+
...
|
|
19
|
+
... def detect(self, text: str) -> DetectionResult:
|
|
20
|
+
... return DetectionResult("mybackend", "en", 0.9)
|
|
21
|
+
...
|
|
22
|
+
... def supported_languages(self) -> set[str]:
|
|
23
|
+
... return {"en", "fr", "es"}
|
|
24
|
+
>>>
|
|
25
|
+
>>> # Now use it
|
|
26
|
+
>>> from fastlangml import detect
|
|
27
|
+
>>> detect("Hello", backends=["mybackend"]).lang
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
34
|
+
|
|
35
|
+
from fastlangml.backends.base import Backend, DetectionResult
|
|
36
|
+
from fastlangml.exceptions import BackendUnavailableError
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
T = TypeVar("T", bound=type[Backend])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Backend Registry
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
_CUSTOM_BACKEND_REGISTRY: dict[str, type[Backend]] = {}
|
|
49
|
+
|
|
50
|
+
_BACKEND_RELIABILITY: dict[str, int] = {
|
|
51
|
+
"fasttext": 5,
|
|
52
|
+
"lingua": 4,
|
|
53
|
+
"pycld3": 3,
|
|
54
|
+
"langdetect": 2,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_BUILTIN_BACKEND_NAMES = frozenset({"fasttext", "langdetect", "lingua", "pycld3"})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def backend(name: str, reliability: int = 3) -> Callable[[T], T]:
|
|
61
|
+
"""Decorator to register a custom backend.
|
|
62
|
+
|
|
63
|
+
Use this decorator on a class extending Backend to register it
|
|
64
|
+
for use in FastLangID's ensemble detection.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
name: Unique identifier for the backend.
|
|
68
|
+
reliability: Reliability score 1-5 (higher = more weight in voting).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Decorator function that registers the class.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> from fastlangml import backend, Backend
|
|
75
|
+
>>> from fastlangml.backends.base import DetectionResult
|
|
76
|
+
>>>
|
|
77
|
+
>>> @backend("openai", reliability=5)
|
|
78
|
+
... class OpenAIBackend(Backend):
|
|
79
|
+
... '''Custom backend using OpenAI API.'''
|
|
80
|
+
...
|
|
81
|
+
... @property
|
|
82
|
+
... def name(self) -> str:
|
|
83
|
+
... return "openai"
|
|
84
|
+
...
|
|
85
|
+
... @property
|
|
86
|
+
... def is_available(self) -> bool:
|
|
87
|
+
... try:
|
|
88
|
+
... import openai
|
|
89
|
+
... return True
|
|
90
|
+
... except ImportError:
|
|
91
|
+
... return False
|
|
92
|
+
...
|
|
93
|
+
... def detect(self, text: str) -> DetectionResult:
|
|
94
|
+
... # Your OpenAI API call here
|
|
95
|
+
... lang, conf = self._detect_with_openai(text)
|
|
96
|
+
... return DetectionResult(self.name, lang, conf)
|
|
97
|
+
...
|
|
98
|
+
... def supported_languages(self) -> set[str]:
|
|
99
|
+
... return {"en", "fr", "de", "es", "zh", "ja", "ko"}
|
|
100
|
+
>>>
|
|
101
|
+
>>> # Use in detection
|
|
102
|
+
>>> from fastlangml import FastLangDetector, DetectionConfig
|
|
103
|
+
>>> detector = FastLangDetector(
|
|
104
|
+
... config=DetectionConfig(backends=["openai", "fasttext"])
|
|
105
|
+
... )
|
|
106
|
+
"""
|
|
107
|
+
def decorator(cls: T) -> T:
|
|
108
|
+
register_backend(name, cls, reliability)
|
|
109
|
+
return cls
|
|
110
|
+
return decorator
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def register_backend(
|
|
114
|
+
name: str,
|
|
115
|
+
backend_class: type[Backend],
|
|
116
|
+
reliability: int = 3,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Register a custom backend (function form).
|
|
119
|
+
|
|
120
|
+
Prefer using the @backend decorator for cleaner code.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: Unique identifier for the backend.
|
|
124
|
+
backend_class: Class extending Backend.
|
|
125
|
+
reliability: Reliability score 1-5.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If name conflicts with built-in or reliability out of range.
|
|
129
|
+
TypeError: If backend_class doesn't extend Backend.
|
|
130
|
+
"""
|
|
131
|
+
if name in _BUILTIN_BACKEND_NAMES:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Cannot register '{name}': conflicts with built-in backend. "
|
|
134
|
+
f"Built-in: {sorted(_BUILTIN_BACKEND_NAMES)}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if not isinstance(backend_class, type) or not issubclass(backend_class, Backend):
|
|
138
|
+
raise TypeError(
|
|
139
|
+
f"backend_class must extend Backend, got {type(backend_class)}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if not isinstance(reliability, int) or not 1 <= reliability <= 5:
|
|
143
|
+
raise ValueError(f"reliability must be 1-5, got {reliability}")
|
|
144
|
+
|
|
145
|
+
_CUSTOM_BACKEND_REGISTRY[name] = backend_class
|
|
146
|
+
_BACKEND_RELIABILITY[name] = reliability
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def unregister_backend(name: str) -> bool:
|
|
150
|
+
"""Remove a registered custom backend.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
name: Backend name to remove.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if removed, False if not found.
|
|
157
|
+
"""
|
|
158
|
+
if name in _BUILTIN_BACKEND_NAMES:
|
|
159
|
+
raise ValueError(f"Cannot unregister built-in backend: {name}")
|
|
160
|
+
|
|
161
|
+
removed = _CUSTOM_BACKEND_REGISTRY.pop(name, None) is not None
|
|
162
|
+
_BACKEND_RELIABILITY.pop(name, None)
|
|
163
|
+
return removed
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def list_registered_backends() -> list[str]:
|
|
167
|
+
"""List custom registered backends."""
|
|
168
|
+
return list(_CUSTOM_BACKEND_REGISTRY.keys())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def clear_registered_backends() -> None:
|
|
172
|
+
"""Remove all custom backends (useful for testing)."""
|
|
173
|
+
for name in list(_CUSTOM_BACKEND_REGISTRY.keys()):
|
|
174
|
+
unregister_backend(name)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# =============================================================================
|
|
178
|
+
# Backend Factory
|
|
179
|
+
# =============================================================================
|
|
180
|
+
|
|
181
|
+
def _get_backend_class(name: str) -> type[Backend]:
|
|
182
|
+
"""Get backend class by name (lazy import to avoid loading unavailable deps)."""
|
|
183
|
+
# Check custom registry first
|
|
184
|
+
if name in _CUSTOM_BACKEND_REGISTRY:
|
|
185
|
+
return _CUSTOM_BACKEND_REGISTRY[name]
|
|
186
|
+
|
|
187
|
+
# Built-in backends (lazy import)
|
|
188
|
+
if name == "fasttext":
|
|
189
|
+
from fastlangml.backends.fasttext_backend import FastTextBackend
|
|
190
|
+
return FastTextBackend
|
|
191
|
+
elif name == "langdetect":
|
|
192
|
+
from fastlangml.backends.langdetect_backend import LangdetectBackend
|
|
193
|
+
return LangdetectBackend
|
|
194
|
+
elif name == "lingua":
|
|
195
|
+
from fastlangml.backends.lingua_backend import LinguaBackend
|
|
196
|
+
return LinguaBackend
|
|
197
|
+
elif name == "pycld3":
|
|
198
|
+
from fastlangml.backends.pycld3_backend import PyCLD3Backend
|
|
199
|
+
return PyCLD3Backend
|
|
200
|
+
else:
|
|
201
|
+
available = sorted(_BUILTIN_BACKEND_NAMES | set(_CUSTOM_BACKEND_REGISTRY.keys()))
|
|
202
|
+
raise ValueError(f"Unknown backend: {name}. Available: {available}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# For backward compatibility
|
|
206
|
+
BACKEND_NAMES = _BUILTIN_BACKEND_NAMES
|
|
207
|
+
BACKEND_RELIABILITY = _BACKEND_RELIABILITY
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def create_backend(name: str, **kwargs: object) -> Backend:
|
|
211
|
+
"""
|
|
212
|
+
Create a backend instance by name.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
name: Backend name ("fasttext", "langdetect", "lingua", "pycld3", "langid")
|
|
216
|
+
**kwargs: Backend-specific configuration
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
BackendUnavailableError: If backend dependencies not installed
|
|
220
|
+
ValueError: If backend name is unknown
|
|
221
|
+
"""
|
|
222
|
+
backend_class = _get_backend_class(name)
|
|
223
|
+
backend = backend_class(**kwargs) # type: ignore[arg-type]
|
|
224
|
+
|
|
225
|
+
if not backend.is_available:
|
|
226
|
+
install_cmd = f"pip install fastlang[{name}]"
|
|
227
|
+
raise BackendUnavailableError(
|
|
228
|
+
f"Backend '{name}' is not available. Install with: {install_cmd}",
|
|
229
|
+
backend_name=name,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return backend
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_available_backends() -> list[str]:
|
|
236
|
+
"""Return list of available backend names (built-in + custom)."""
|
|
237
|
+
available = []
|
|
238
|
+
|
|
239
|
+
# Check built-in backends
|
|
240
|
+
for name in _BUILTIN_BACKEND_NAMES:
|
|
241
|
+
try:
|
|
242
|
+
backend_class = _get_backend_class(name)
|
|
243
|
+
backend_instance = backend_class()
|
|
244
|
+
if backend_instance.is_available:
|
|
245
|
+
available.append(name)
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
# Check custom backends
|
|
250
|
+
for name, backend_class in _CUSTOM_BACKEND_REGISTRY.items():
|
|
251
|
+
try:
|
|
252
|
+
backend_instance = backend_class()
|
|
253
|
+
if backend_instance.is_available:
|
|
254
|
+
available.append(name)
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
return sorted(available, key=lambda x: _BACKEND_RELIABILITY.get(x, 0), reverse=True)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_backend_reliability(name: str) -> int:
|
|
262
|
+
"""Get reliability score for a backend (higher = more reliable)."""
|
|
263
|
+
return BACKEND_RELIABILITY.get(name, 0)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
__all__ = [
|
|
267
|
+
# Base classes
|
|
268
|
+
"Backend",
|
|
269
|
+
"DetectionResult",
|
|
270
|
+
# Factory functions
|
|
271
|
+
"create_backend",
|
|
272
|
+
"get_available_backends",
|
|
273
|
+
"get_backend_reliability",
|
|
274
|
+
# Custom backend registration
|
|
275
|
+
"backend", # Decorator (preferred)
|
|
276
|
+
"register_backend",
|
|
277
|
+
"unregister_backend",
|
|
278
|
+
"list_registered_backends",
|
|
279
|
+
"clear_registered_backends",
|
|
280
|
+
# Constants
|
|
281
|
+
"BACKEND_NAMES",
|
|
282
|
+
"BACKEND_RELIABILITY",
|
|
283
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Abstract base class for language detection backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DetectionResult:
|
|
11
|
+
"""Result from a single backend detection."""
|
|
12
|
+
|
|
13
|
+
backend_name: str
|
|
14
|
+
language: str
|
|
15
|
+
confidence: float
|
|
16
|
+
all_probabilities: dict[str, float] = field(default_factory=dict)
|
|
17
|
+
is_reliable: bool = True # Backend-specific reliability flag
|
|
18
|
+
|
|
19
|
+
def __post_init__(self) -> None:
|
|
20
|
+
# Normalize language code to lowercase
|
|
21
|
+
self.language = self.language.lower() if self.language else "unknown"
|
|
22
|
+
# Ensure all_probabilities includes the top result
|
|
23
|
+
if self.language != "unknown" and self.language not in self.all_probabilities:
|
|
24
|
+
self.all_probabilities[self.language] = self.confidence
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Backend(ABC):
|
|
28
|
+
"""Abstract base class for language detection backends."""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def name(self) -> str:
|
|
33
|
+
"""Backend identifier."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def is_available(self) -> bool:
|
|
39
|
+
"""Check if backend dependencies are installed."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def detect(self, text: str) -> DetectionResult:
|
|
44
|
+
"""
|
|
45
|
+
Detect language of text.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
text: Preprocessed text to analyze
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
DetectionResult with language code, confidence, and all probabilities
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def supported_languages(self) -> set[str]:
|
|
57
|
+
"""Return set of ISO 639-1 codes this backend supports."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def detect_batch(self, texts: list[str]) -> list[DetectionResult]:
|
|
61
|
+
"""
|
|
62
|
+
Detect language for multiple texts.
|
|
63
|
+
|
|
64
|
+
Default implementation calls detect() for each text.
|
|
65
|
+
Backends can override for optimized batch processing.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
texts: List of texts to analyze
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of DetectionResults
|
|
72
|
+
"""
|
|
73
|
+
return [self.detect(text) for text in texts]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""FastText-based language detection backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastlangml.backends.base import Backend, DetectionResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FastTextBackend(Backend):
|
|
9
|
+
"""
|
|
10
|
+
FastText-based language detection backend.
|
|
11
|
+
|
|
12
|
+
Uses the fasttext-langdetect package which wraps Facebook's FastText
|
|
13
|
+
language identification model trained on Wikipedia data.
|
|
14
|
+
|
|
15
|
+
Supports 176 languages with high accuracy.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, low_memory: bool = False) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Args:
|
|
21
|
+
low_memory: Use low memory mode (slower but uses less RAM)
|
|
22
|
+
"""
|
|
23
|
+
self._low_memory = low_memory
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def name(self) -> str:
|
|
27
|
+
return "fasttext"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def is_available(self) -> bool:
|
|
31
|
+
try:
|
|
32
|
+
from ftlangdetect import detect # noqa: F401
|
|
33
|
+
|
|
34
|
+
return True
|
|
35
|
+
except ImportError:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def detect(self, text: str) -> DetectionResult:
|
|
39
|
+
from ftlangdetect import detect
|
|
40
|
+
|
|
41
|
+
# Clean text (fasttext expects single line)
|
|
42
|
+
clean_text = " ".join(text.split())
|
|
43
|
+
|
|
44
|
+
if not clean_text:
|
|
45
|
+
return DetectionResult(
|
|
46
|
+
backend_name=self.name,
|
|
47
|
+
language="unknown",
|
|
48
|
+
confidence=0.0,
|
|
49
|
+
all_probabilities={},
|
|
50
|
+
is_reliable=False,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
result = detect(text=clean_text, low_memory=self._low_memory)
|
|
55
|
+
|
|
56
|
+
lang = result.get("lang", "unknown")
|
|
57
|
+
score = float(result.get("score", 0.0))
|
|
58
|
+
|
|
59
|
+
return DetectionResult(
|
|
60
|
+
backend_name=self.name,
|
|
61
|
+
language=lang,
|
|
62
|
+
confidence=score,
|
|
63
|
+
all_probabilities={lang: score},
|
|
64
|
+
is_reliable=score > 0.5,
|
|
65
|
+
)
|
|
66
|
+
except Exception:
|
|
67
|
+
return DetectionResult(
|
|
68
|
+
backend_name=self.name,
|
|
69
|
+
language="unknown",
|
|
70
|
+
confidence=0.0,
|
|
71
|
+
all_probabilities={},
|
|
72
|
+
is_reliable=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def supported_languages(self) -> set[str]:
|
|
76
|
+
"""FastText supports 176 languages."""
|
|
77
|
+
return {
|
|
78
|
+
"af", "als", "am", "an", "ar", "arz", "as", "ast", "av", "az",
|
|
79
|
+
"azb", "ba", "bar", "bcl", "be", "bg", "bh", "bn", "bo", "bpy",
|
|
80
|
+
"br", "bs", "bxr", "ca", "cbk", "ce", "ceb", "ckb", "co", "cs",
|
|
81
|
+
"cv", "cy", "da", "de", "diq", "dsb", "dty", "dv", "el", "eml",
|
|
82
|
+
"en", "eo", "es", "et", "eu", "fa", "fi", "fr", "frr", "fy",
|
|
83
|
+
"ga", "gd", "gl", "gn", "gom", "gu", "gv", "he", "hi", "hif",
|
|
84
|
+
"hr", "hsb", "ht", "hu", "hy", "ia", "id", "ie", "ilo", "io",
|
|
85
|
+
"is", "it", "ja", "jbo", "jv", "ka", "kk", "km", "kn", "ko",
|
|
86
|
+
"krc", "ku", "kv", "kw", "ky", "la", "lb", "lez", "li", "lmo",
|
|
87
|
+
"lo", "lrc", "lt", "lv", "mai", "mg", "mhr", "min", "mk", "ml",
|
|
88
|
+
"mn", "mr", "mrj", "ms", "mt", "mwl", "my", "myv", "mzn", "nah",
|
|
89
|
+
"nap", "nds", "ne", "new", "nl", "nn", "no", "oc", "or", "os",
|
|
90
|
+
"pa", "pam", "pfl", "pl", "pms", "pnb", "ps", "pt", "qu", "rm",
|
|
91
|
+
"ro", "ru", "rue", "sa", "sah", "sc", "scn", "sco", "sd", "sh",
|
|
92
|
+
"si", "sk", "sl", "so", "sq", "sr", "su", "sv", "sw", "ta",
|
|
93
|
+
"te", "tg", "th", "tk", "tl", "tr", "tt", "tyv", "ug", "uk",
|
|
94
|
+
"ur", "uz", "vec", "vep", "vi", "vls", "vo", "wa", "war", "wuu",
|
|
95
|
+
"xal", "xmf", "yi", "yo", "yue", "zh",
|
|
96
|
+
}
|