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 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
+ }