fast-langdetect 0.2.2__tar.gz → 0.2.4__tar.gz

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.
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fast-langdetect
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Quickly detect text language and segment language
5
5
  Author-Email: sudoskys <coldlando@hotmail.com>
6
6
  License: MIT
7
7
  Requires-Python: <3.13,>=3.9
8
- Requires-Dist: fasttext-wheel>=0.9.2
9
8
  Requires-Dist: robust-downloader>=0.0.2
10
- Requires-Dist: numpy<2.0.0,>=1.26.4
11
9
  Requires-Dist: requests>=2.32.3
10
+ Requires-Dist: fasttext-predict>=0.9.2.4
12
11
  Description-Content-Type: text/markdown
13
12
 
14
13
  # fast-langdetect 🚀
@@ -19,21 +18,20 @@ Description-Content-Type: text/markdown
19
18
 
20
19
  ## Overview
21
20
 
22
- **fast-langdetect** provides ultra-fast and highly accurate language detection based on FastText, a library developed by
23
- Facebook. This package is 80x faster than traditional methods and offers 95% accuracy.
21
+ **`fast-langdetect`** is an ultra-fast and highly accurate language detection library based on FastText, a library developed by Facebook. Its incredible speed and accuracy make it 80x faster than conventional methods and deliver up to 95% accuracy.
24
22
 
25
- It supports Python versions 3.9 to 3.12.
23
+ - Supported Python `3.9` to `3.12`.
24
+ - Works offline in low memory mode
25
+ - No `numpy` required (thanks to @dalf).
26
26
 
27
- Support offline usage.
27
+ > ### Background
28
+ >
29
+ > This project builds upon [zafercavdar/fasttext-langdetect](https://github.com/zafercavdar/fasttext-langdetect#benchmark) with enhancements in packaging.
30
+ > For more information about the underlying model, see the official FastText documentation: [Language Identification](https://fasttext.cc/docs/en/language-identification.html).
28
31
 
29
- This project builds upon [zafercavdar/fasttext-langdetect](https://github.com/zafercavdar/fasttext-langdetect#benchmark)
30
- with enhancements in packaging.
31
-
32
- For more information on the underlying FastText model, refer to the official
33
- documentation: [FastText Language Identification](https://fasttext.cc/docs/en/language-identification.html).
34
-
35
- > [!NOTE]
36
- > This library requires over 200MB of memory to use in low memory mode.
32
+ > ### Possible memory usage
33
+ >
34
+ > *This library requires at least **200MB memory** in low-memory mode.*
37
35
 
38
36
  ## Installation 💻
39
37
 
@@ -53,20 +51,17 @@ pdm add fast-langdetect
53
51
 
54
52
  ## Usage 🖥️
55
53
 
56
- For optimal performance and accuracy in language detection, use `detect(text, low_memory=False)` to load the larger
57
- model.
54
+ In scenarios **where accuracy is important**, you should not rely on the detection results of small models, use `low_memory=False` to download larger models!
58
55
 
59
- > The model will be downloaded to the `/tmp/fasttext-langdetect` directory upon first use.
56
+ ### Prerequisites
60
57
 
61
- ### Native API (Recommended)
58
+ - The “/n” character in the argument string must be removed before calling the function.
59
+ - If the sample is too long or too short, the accuracy will be reduced (e.g. if it is too short, Chinese will be predicted as Japanese).
60
+ - The model will be downloaded to the `/tmp/fasttext-langdetect` directory upon first use.
62
61
 
63
- > [!NOTE]
64
- > This function assumes to be given a single line of text. *You should remove `\n` characters before passing the text.*
65
- > If the sample is too long or too short, the accuracy will decrease (for example, in the case of too short, Chinese
66
- > will be predicted as Japanese).
62
+ ### Native API (Recommended)
67
63
 
68
64
  ```python
69
-
70
65
  from fast_langdetect import detect, detect_multilingual
71
66
 
72
67
  # Single language detection
@@ -82,15 +77,17 @@ multiline_text = """
82
77
  Hello, world!
83
78
  This is a multiline text.
84
79
  But we need remove `\n` characters or it will raise an ValueError.
80
+ REMOVE \n
85
81
  """
86
- multiline_text = multiline_text.replace("\n", "") # NOTE:ITS IMPORTANT TO REMOVE \n CHARACTERS
82
+ multiline_text = multiline_text.replace("\n", "")
87
83
  print(detect(multiline_text))
88
84
  # Output: {'lang': 'en', 'score': 0.8509423136711121}
89
85
 
90
86
  print(detect("Привет, мир!")["lang"])
91
87
  # Output: ru
92
88
 
93
- # Multi-language detection
89
+ # Multi-language detection with low memory mode enabled
90
+ # The accuracy is not as good as it should be
94
91
  print(detect_multilingual("Hello, world!你好世界!Привет, мир!"))
95
92
  # Output: [{'lang': 'ja', 'score': 0.32009604573249817}, {'lang': 'uk', 'score': 0.27781224250793457}, {'lang': 'zh', 'score': 0.17542070150375366}, {'lang': 'sr', 'score': 0.08751443773508072}, {'lang': 'bg', 'score': 0.05222449079155922}]
96
93
 
@@ -99,6 +96,10 @@ print(detect_multilingual("Hello, world!你好世界!Привет, мир!", low
99
96
  # Output: [{'lang': 'ru', 'score': 0.39008623361587524}, {'lang': 'zh', 'score': 0.18235979974269867}, {'lang': 'ja', 'score': 0.08473210036754608}, {'lang': 'sr', 'score': 0.057975586503744125}, {'lang': 'en', 'score': 0.05422825738787651}]
100
97
  ```
101
98
 
99
+ #### Fallbacks
100
+
101
+ We provide a fallback mechanism: when `use_strict_mode=False`, if the program fails to load the **large model** (`low_memory=False`), it will fall back to the offline **small model** to complete the prediction task.
102
+
102
103
  ### Convenient `detect_language` Function
103
104
 
104
105
  ```python
@@ -148,4 +149,4 @@ models
148
149
  journal={arXiv preprint arXiv:1612.03651},
149
150
  year={2016}
150
151
  }
151
- ```
152
+ ```
@@ -6,21 +6,20 @@
6
6
 
7
7
  ## Overview
8
8
 
9
- **fast-langdetect** provides ultra-fast and highly accurate language detection based on FastText, a library developed by
10
- Facebook. This package is 80x faster than traditional methods and offers 95% accuracy.
9
+ **`fast-langdetect`** is an ultra-fast and highly accurate language detection library based on FastText, a library developed by Facebook. Its incredible speed and accuracy make it 80x faster than conventional methods and deliver up to 95% accuracy.
11
10
 
12
- It supports Python versions 3.9 to 3.12.
11
+ - Supported Python `3.9` to `3.12`.
12
+ - Works offline in low memory mode
13
+ - No `numpy` required (thanks to @dalf).
13
14
 
14
- Support offline usage.
15
+ > ### Background
16
+ >
17
+ > This project builds upon [zafercavdar/fasttext-langdetect](https://github.com/zafercavdar/fasttext-langdetect#benchmark) with enhancements in packaging.
18
+ > For more information about the underlying model, see the official FastText documentation: [Language Identification](https://fasttext.cc/docs/en/language-identification.html).
15
19
 
16
- This project builds upon [zafercavdar/fasttext-langdetect](https://github.com/zafercavdar/fasttext-langdetect#benchmark)
17
- with enhancements in packaging.
18
-
19
- For more information on the underlying FastText model, refer to the official
20
- documentation: [FastText Language Identification](https://fasttext.cc/docs/en/language-identification.html).
21
-
22
- > [!NOTE]
23
- > This library requires over 200MB of memory to use in low memory mode.
20
+ > ### Possible memory usage
21
+ >
22
+ > *This library requires at least **200MB memory** in low-memory mode.*
24
23
 
25
24
  ## Installation 💻
26
25
 
@@ -40,20 +39,17 @@ pdm add fast-langdetect
40
39
 
41
40
  ## Usage 🖥️
42
41
 
43
- For optimal performance and accuracy in language detection, use `detect(text, low_memory=False)` to load the larger
44
- model.
42
+ In scenarios **where accuracy is important**, you should not rely on the detection results of small models, use `low_memory=False` to download larger models!
45
43
 
46
- > The model will be downloaded to the `/tmp/fasttext-langdetect` directory upon first use.
44
+ ### Prerequisites
47
45
 
48
- ### Native API (Recommended)
46
+ - The “/n” character in the argument string must be removed before calling the function.
47
+ - If the sample is too long or too short, the accuracy will be reduced (e.g. if it is too short, Chinese will be predicted as Japanese).
48
+ - The model will be downloaded to the `/tmp/fasttext-langdetect` directory upon first use.
49
49
 
50
- > [!NOTE]
51
- > This function assumes to be given a single line of text. *You should remove `\n` characters before passing the text.*
52
- > If the sample is too long or too short, the accuracy will decrease (for example, in the case of too short, Chinese
53
- > will be predicted as Japanese).
50
+ ### Native API (Recommended)
54
51
 
55
52
  ```python
56
-
57
53
  from fast_langdetect import detect, detect_multilingual
58
54
 
59
55
  # Single language detection
@@ -69,15 +65,17 @@ multiline_text = """
69
65
  Hello, world!
70
66
  This is a multiline text.
71
67
  But we need remove `\n` characters or it will raise an ValueError.
68
+ REMOVE \n
72
69
  """
73
- multiline_text = multiline_text.replace("\n", "") # NOTE:ITS IMPORTANT TO REMOVE \n CHARACTERS
70
+ multiline_text = multiline_text.replace("\n", "")
74
71
  print(detect(multiline_text))
75
72
  # Output: {'lang': 'en', 'score': 0.8509423136711121}
76
73
 
77
74
  print(detect("Привет, мир!")["lang"])
78
75
  # Output: ru
79
76
 
80
- # Multi-language detection
77
+ # Multi-language detection with low memory mode enabled
78
+ # The accuracy is not as good as it should be
81
79
  print(detect_multilingual("Hello, world!你好世界!Привет, мир!"))
82
80
  # Output: [{'lang': 'ja', 'score': 0.32009604573249817}, {'lang': 'uk', 'score': 0.27781224250793457}, {'lang': 'zh', 'score': 0.17542070150375366}, {'lang': 'sr', 'score': 0.08751443773508072}, {'lang': 'bg', 'score': 0.05222449079155922}]
83
81
 
@@ -86,6 +84,10 @@ print(detect_multilingual("Hello, world!你好世界!Привет, мир!", low
86
84
  # Output: [{'lang': 'ru', 'score': 0.39008623361587524}, {'lang': 'zh', 'score': 0.18235979974269867}, {'lang': 'ja', 'score': 0.08473210036754608}, {'lang': 'sr', 'score': 0.057975586503744125}, {'lang': 'en', 'score': 0.05422825738787651}]
87
85
  ```
88
86
 
87
+ #### Fallbacks
88
+
89
+ We provide a fallback mechanism: when `use_strict_mode=False`, if the program fails to load the **large model** (`low_memory=False`), it will fall back to the offline **small model** to complete the prediction task.
90
+
89
91
  ### Convenient `detect_language` Function
90
92
 
91
93
  ```python
@@ -135,4 +137,4 @@ models
135
137
  journal={arXiv preprint arXiv:1612.03651},
136
138
  year={2016}
137
139
  }
138
- ```
140
+ ```
@@ -1,15 +1,14 @@
1
1
  [project]
2
2
  name = "fast-langdetect"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Quickly detect text language and segment language"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
7
7
  ]
8
8
  dependencies = [
9
- "fasttext-wheel>=0.9.2",
10
9
  "robust-downloader>=0.0.2",
11
- "numpy>=1.26.4,<2.0.0",
12
10
  "requests>=2.32.3",
11
+ "fasttext-predict>=0.9.2.4",
13
12
  ]
14
13
  requires-python = ">=3.9,<3.13"
15
14
  readme = "README.md"
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+
4
+ from .ft_detect import detect, detect_language, detect_multilingual # noqa: F401
@@ -1,6 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # @Time : 2024/1/17 下午4:00
3
- import logging
4
3
 
5
4
  from .infer import detect
6
5
  from .infer import detect_multilingual # noqa: F401
@@ -24,14 +23,3 @@ def detect_language(sentence, *, low_memory: bool = True):
24
23
  if lang_code == "JA" and not is_japanese(sentence):
25
24
  lang_code = "ZH"
26
25
  return lang_code
27
-
28
-
29
- def detect_langs(sentence, *, low_memory: bool = True):
30
- """
31
- Detect language
32
- :param sentence: str sentence
33
- :param low_memory: bool (default: True) whether to use low memory mode
34
- :return: ZH, EN, JA, KO, FR, DE, ES, .... (two uppercase letters)
35
- """
36
- logging.warning("detect_langs is deprecated, use detect_language instead")
37
- return detect_language(sentence, low_memory=low_memory)
@@ -0,0 +1,261 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/01/17 下午08:30
3
+ # @Author : sudoskys
4
+ # @File : infer.py
5
+ # @Software: PyCharm
6
+ import logging
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional, Union, Any
10
+
11
+ import fasttext
12
+ from robust_downloader import download
13
+
14
+ logger = logging.getLogger(__name__)
15
+ CACHE_DIRECTORY = os.getenv("FTLANG_CACHE", "/tmp/fasttext-langdetect")
16
+ LOCAL_SMALL_MODEL_PATH = Path(__file__).parent / "resources" / "lid.176.ftz"
17
+
18
+ FASTTEXT_LARGE_MODEL_URL = "https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin"
19
+ FASTTEXT_LARGE_MODEL_NAME = "lid.176.bin"
20
+ VERIFY_FASTTEXT_LARGE_MODEL = "01810bc59c6a3d2b79c79e6336612f65"
21
+
22
+
23
+ class DetectError(Exception):
24
+ pass
25
+
26
+
27
+ class ModelManager:
28
+ def __init__(self):
29
+ self._models = {}
30
+
31
+ def get_model(self, key: str):
32
+ """Retrieve cached model."""
33
+ return self._models.get(key)
34
+
35
+ def cache_model(self, key: str, model) -> None:
36
+ """Cache loaded FastText model."""
37
+ self._models[key] = model
38
+
39
+
40
+ _model_cache = ModelManager()
41
+
42
+ import hashlib
43
+
44
+
45
+ def calculate_md5(file_path, chunk_size=8192):
46
+ """
47
+ Calculate the MD5 hash of a file.
48
+
49
+ :param file_path: Path to the file
50
+ :param chunk_size: Size of each chunk to read from the file
51
+ :return: MD5 hash of the file
52
+ """
53
+ md5 = hashlib.md5()
54
+ with open(file_path, 'rb') as f:
55
+ for chunk in iter(lambda: f.read(chunk_size), b''):
56
+ md5.update(chunk)
57
+ return md5.hexdigest()
58
+
59
+
60
+ def verify_md5(file_path, expected_md5, chunk_size=8192):
61
+ """
62
+ Verify the MD5 hash of a file against an expected hash.
63
+
64
+ :param file_path: Path to the file
65
+ :param expected_md5: Expected MD5 hash
66
+ :param chunk_size: Size of each chunk to read from the file
67
+ :return: True if the file's MD5 hash matches the expected hash, False otherwise
68
+ """
69
+ md5 = calculate_md5(file_path, chunk_size)
70
+ return md5 == expected_md5
71
+
72
+
73
+ def download_model(
74
+ download_url: str,
75
+ save_path: Path,
76
+ proxy: Optional[str] = None
77
+ ) -> None:
78
+ """
79
+ Download FastText model file if it doesn't exist.
80
+ :param download_url: URL to download the model from
81
+ :param save_path: Path to save the downloaded model
82
+ :param proxy: Proxy URL for downloading the model
83
+ :raises DetectError: If download fails
84
+ """
85
+ if save_path.exists():
86
+ logger.info(f"fast-langdetect:Model already exists at {save_path}. Skipping download.")
87
+ return
88
+
89
+ logger.info(f"fast-langdetect:Downloading FastText model from {download_url} to {save_path}")
90
+ try:
91
+ download(
92
+ url=download_url,
93
+ folder=str(save_path.parent),
94
+ filename=save_path.name,
95
+ proxy=proxy,
96
+ retry_max=2,
97
+ sleep_max=5,
98
+ timeout=7,
99
+ )
100
+ except Exception as e:
101
+ logger.error(f"fast-langdetect:Failed to download FastText model from {download_url}: {e}")
102
+ raise DetectError(f"Unable to download model from {download_url}")
103
+
104
+
105
+ def load_fasttext_model(
106
+ model_path: Path,
107
+ download_url: Optional[str] = None,
108
+ proxy: Optional[str] = None,
109
+ ):
110
+ """
111
+ Load a FastText model, downloading it if necessary.
112
+ :param model_path: Path to the FastText model file
113
+ :param download_url: URL to download the model from
114
+ :param proxy: Proxy URL for downloading the model
115
+ :return: FastText model
116
+ :raises DetectError: If model loading fails
117
+ """
118
+ if all([
119
+ VERIFY_FASTTEXT_LARGE_MODEL,
120
+ model_path.exists(),
121
+ model_path.name == FASTTEXT_LARGE_MODEL_NAME,
122
+ ]):
123
+ if not verify_md5(model_path, VERIFY_FASTTEXT_LARGE_MODEL):
124
+ logger.warning(
125
+ f"fast-langdetect: MD5 hash verification failed for {model_path}, "
126
+ f"please check the integrity of the downloaded file from {FASTTEXT_LARGE_MODEL_URL}. "
127
+ "\n This may seriously reduce the prediction accuracy. "
128
+ "If you want to ignore this, please set `fast_langdetect.ft_detect.infer.VERIFY_FASTTEXT_LARGE_MODEL = None` "
129
+ )
130
+ if not model_path.exists():
131
+ if download_url:
132
+ download_model(download_url, model_path, proxy)
133
+ if not model_path.exists():
134
+ raise DetectError(f"FastText model file not found at {model_path}")
135
+
136
+ try:
137
+ # Load FastText model
138
+ return fasttext.load_model(str(model_path))
139
+ except Exception as e:
140
+ logger.warning(f"fast-langdetect:Failed to load FastText model from {model_path}: {e}")
141
+ raise DetectError(f"Failed to load FastText model: {e}")
142
+
143
+
144
+ def load_model(
145
+ low_memory: bool = False,
146
+ download_proxy: Optional[str] = None,
147
+ use_strict_mode: bool = False,
148
+ ):
149
+ """
150
+ Load a FastText model based on memory preference.
151
+ :param low_memory: Whether to use a memory-efficient model
152
+ :param download_proxy: Proxy URL for downloading the model
153
+ :param use_strict_mode: If enabled, strictly loads large model or raises error if it fails
154
+ """
155
+ # Model path selection
156
+ if low_memory:
157
+ cache_key = "low_memory"
158
+ model_path = LOCAL_SMALL_MODEL_PATH
159
+ else:
160
+ cache_key = "high_memory"
161
+ model_path = Path(CACHE_DIRECTORY) / FASTTEXT_LARGE_MODEL_NAME
162
+
163
+ # Check cache
164
+ cached_model = _model_cache.get_model(cache_key)
165
+ if cached_model:
166
+ return cached_model
167
+
168
+ # Load appropriate model
169
+ try:
170
+ if low_memory:
171
+ model = load_fasttext_model(model_path)
172
+ else:
173
+ model = load_fasttext_model(model_path, download_url=FASTTEXT_LARGE_MODEL_URL, proxy=download_proxy)
174
+ _model_cache.cache_model(cache_key, model)
175
+ return model
176
+ except Exception as e:
177
+ logger.warning(f"fast-langdetect:Failed to load model ({'low' if low_memory else 'high'} memory): {e}")
178
+ if use_strict_mode:
179
+ raise DetectError("Failed to load FastText model.") from e
180
+ elif not low_memory:
181
+ logger.info("Falling back to low-memory model...")
182
+ return load_model(low_memory=True, use_strict_mode=True)
183
+ raise e
184
+
185
+
186
+ def detect(
187
+ text: str,
188
+ *,
189
+ low_memory: bool = True,
190
+ model_download_proxy: Optional[str] = None,
191
+ use_strict_mode: bool = False,
192
+ ) -> Dict[str, Union[str, float]]:
193
+ """
194
+ Detect the language of a text using FastText.
195
+
196
+ - You MUST manually remove line breaks(`n`) from the text to be processed in advance, otherwise a ValueError is raised.
197
+
198
+ - In scenarios **where accuracy is important**, you should not rely on the detection results of small models, use `low_memory=False` to download larger models!
199
+
200
+ :param text: The text for language detection
201
+ :param low_memory: Whether to use the compressed version of the model (https://fasttext.cc/docs/en/language-identification.html)
202
+ :param model_download_proxy: Download proxy for the model if needed
203
+ :param use_strict_mode: When this parameter is enabled, the fallback after loading failure will be disabled.
204
+ :return: A dictionary with detected language and confidence score
205
+ :raises LanguageDetectionError: If detection fails
206
+ """
207
+ model = load_model(
208
+ low_memory=low_memory,
209
+ download_proxy=model_download_proxy,
210
+ use_strict_mode=use_strict_mode,
211
+ )
212
+ try:
213
+ labels, scores = model.predict(text)
214
+ language_label = labels[0].replace("__label__", "")
215
+ confidence_score = min(float(scores[0]), 1.0)
216
+ return {"lang": language_label, "score": confidence_score}
217
+ except Exception as e:
218
+ logger.error(f"fast-langdetect:Error during language detection: {e}")
219
+ raise DetectError("Language detection failed.")
220
+
221
+
222
+ def detect_multilingual(
223
+ text: str,
224
+ *,
225
+ low_memory: bool = False,
226
+ model_download_proxy: Optional[str] = None,
227
+ k: int = 5,
228
+ threshold: float = 0.0,
229
+ use_strict_mode: bool = False,
230
+ ) -> List[Dict[str, Any]]:
231
+ """
232
+ Detect the top-k probable languages for a given text.
233
+
234
+ - You MUST manually remove line breaks(`n`) from the text to be processed in advance, otherwise a ValueError is raised.
235
+
236
+ - In scenarios **where accuracy is important**, you should not rely on the detection results of small models, use `low_memory=False` to download larger models!
237
+
238
+ :param text: The text for language detection
239
+ :param low_memory: Whether to use the compressed version of the model (https://fasttext.cc/docs/en/language-identification.html)
240
+ :param model_download_proxy: Download proxy for the model if needed
241
+ :param k: Number of top languages to return
242
+ :param threshold: Minimum confidence score to consider
243
+ :param use_strict_mode: When this parameter is enabled, the fallback after loading failure will be disabled.
244
+ :return: A list of dictionaries with detected languages and confidence scores
245
+ """
246
+ model = load_model(
247
+ low_memory=low_memory,
248
+ download_proxy=model_download_proxy,
249
+ use_strict_mode=use_strict_mode,
250
+ )
251
+ # Multilingual detection using FastText
252
+ try:
253
+ labels, scores = model.predict(text, k=k, threshold=threshold)
254
+ results = [
255
+ {"lang": label.replace("__label__", ""), "score": min(float(score), 1.0)}
256
+ for label, score in zip(labels, scores)
257
+ ]
258
+ return sorted(results, key=lambda x: x["score"], reverse=True)
259
+ except Exception as e:
260
+ logger.error(f"fast-langdetect:Error during multilingual detection: {e}")
261
+ raise DetectError("Multilingual detection failed.")
@@ -1,4 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
-
4
- from .ft_detect import detect, detect_language, detect_langs, detect_multilingual # noqa: F401
@@ -1,198 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # @Time : 2024/1/17 下午8:30
3
- # @Author : sudoskys
4
- # @File : infer.py
5
- # @Software: PyCharm
6
- import logging
7
- import os
8
- from enum import Enum
9
- from pathlib import Path
10
- from typing import Dict, Union, List, Optional, Any
11
-
12
- import fasttext
13
- from robust_downloader import download
14
-
15
- logger = logging.getLogger(__name__)
16
- CACHE_DIRECTORY = os.getenv("FTLANG_CACHE", "/tmp/fasttext-langdetect")
17
- LOCAL_SMALL_MODEL_PATH = Path(__file__).parent / "resources" / "lid.176.ftz"
18
-
19
- # Suppress FastText output if possible
20
- try:
21
- fasttext.FastText.eprint = lambda *args, **kwargs: None
22
- except Exception:
23
- pass
24
-
25
-
26
- class ModelType(Enum):
27
- LOW_MEMORY = "low_mem"
28
- HIGH_MEMORY = "high_mem"
29
-
30
-
31
- class ModelCache:
32
- def __init__(self):
33
- self._models = {}
34
-
35
- def get_model(self, model_type: ModelType) -> Optional["fasttext.FastText._FastText"]:
36
- return self._models.get(model_type)
37
-
38
- def set_model(self, model_type: ModelType, model: "fasttext.FastText._FastText"):
39
- self._models[model_type] = model
40
-
41
-
42
- _model_cache = ModelCache()
43
-
44
-
45
- class DetectError(Exception):
46
- """Custom exception for language detection errors."""
47
- pass
48
-
49
-
50
- def load_model(low_memory: bool = False,
51
- download_proxy: Optional[str] = None,
52
- use_strict_mode: bool = False) -> "fasttext.FastText._FastText":
53
- """
54
- Load the FastText model based on memory preference.
55
-
56
- :param low_memory: Indicates whether to load a smaller, memory-efficient model
57
- :param download_proxy: Proxy to use for downloading the large model if necessary
58
- :param use_strict_mode: If enabled, strictly loads large model or raises error if it fails
59
- :return: Loaded FastText model
60
- :raises DetectError: If the model cannot be loaded
61
- """
62
- model_type = ModelType.LOW_MEMORY if low_memory else ModelType.HIGH_MEMORY
63
-
64
- # If the model is already loaded, return it
65
- cached_model = _model_cache.get_model(model_type)
66
- if cached_model:
67
- return cached_model
68
-
69
- def load_local_small_model():
70
- """Try to load the local small model."""
71
- try:
72
- _loaded_model = fasttext.load_model(str(LOCAL_SMALL_MODEL_PATH))
73
- _model_cache.set_model(ModelType.LOW_MEMORY, _loaded_model)
74
- return _loaded_model
75
- except Exception as e:
76
- logger.error(f"Failed to load the local small model '{LOCAL_SMALL_MODEL_PATH}': {e}")
77
- raise DetectError("Unable to load low-memory model from local resources.")
78
-
79
- def load_large_model():
80
- """Try to load the large model."""
81
- try:
82
- loaded_model = fasttext.load_model(str(model_path))
83
- _model_cache.set_model(ModelType.HIGH_MEMORY, loaded_model)
84
- return loaded_model
85
- except Exception as e:
86
- logger.error(f"Failed to load the large model '{model_path}': {e}")
87
- return None
88
-
89
- if low_memory:
90
- # Attempt to load the local small model
91
- return load_local_small_model()
92
-
93
- # Path for the large model
94
- large_model_name = "lid.176.bin"
95
- model_path = Path(CACHE_DIRECTORY) / large_model_name
96
-
97
- # If the large model is already present, load it
98
- if model_path.exists():
99
- # Model cant be dir
100
- if model_path.is_dir():
101
- try:
102
- model_path.rmdir()
103
- except Exception as e:
104
- logger.error(f"Failed to remove the directory '{model_path}': {e}")
105
- raise DetectError(f"Unexpected directory found in large model file path '{model_path}': {e}")
106
- # Attempt to load large model
107
- loaded_model = load_large_model()
108
- if loaded_model:
109
- return loaded_model
110
-
111
- # If the large model is not present, attempt to download (only if necessary)
112
- model_url = "https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin"
113
- try:
114
- logger.info(f"Downloading large model from {model_url} to {model_path}")
115
- download(
116
- url=model_url,
117
- folder=CACHE_DIRECTORY,
118
- filename=large_model_name,
119
- proxy=download_proxy,
120
- retry_max=3,
121
- timeout=20
122
- )
123
- # Try loading the model again after download
124
- loaded_model = load_large_model()
125
- if loaded_model:
126
- return loaded_model
127
- except Exception as e:
128
- logger.error(f"Failed to download the large model: {e}")
129
-
130
- # Handle fallback logic for strict and non-strict modes
131
- if use_strict_mode:
132
- raise DetectError("Strict mode enabled: Unable to download or load the large model.")
133
- else:
134
- logger.info("Attempting to fall back to local small model.")
135
- return load_local_small_model()
136
-
137
-
138
- def detect(text: str, *,
139
- low_memory: bool = True,
140
- model_download_proxy: Optional[str] = None,
141
- use_strict_mode: bool = False
142
- ) -> Dict[str, Union[str, float]]:
143
- """
144
- Detect the language of a text using FastText.
145
- This function assumes to be given a single line of text. We split words on whitespace (space, newline, tab, vertical tab) and the control characters carriage return, formfeed and the null character.
146
- If the model is not supervised, this function will throw a ValueError.
147
- :param text: The text for language detection
148
- :param low_memory: Whether to use a memory-efficient model
149
- :param model_download_proxy: Download proxy for the model if needed
150
- :param use_strict_mode: If enabled, strictly loads large model or raises error if it fails
151
- :return: A dictionary with detected language and confidence score
152
- :raises LanguageDetectionError: If detection fails
153
- """
154
- model = load_model(low_memory=low_memory, download_proxy=model_download_proxy, use_strict_mode=use_strict_mode)
155
- labels, scores = model.predict(text)
156
- language_label = labels[0].replace("__label__", '')
157
- confidence_score = min(float(scores[0]), 1.0)
158
- return {
159
- "lang": language_label,
160
- "score": confidence_score,
161
- }
162
-
163
-
164
- def detect_multilingual(text: str, *,
165
- low_memory: bool = True,
166
- model_download_proxy: Optional[str] = None,
167
- k: int = 5,
168
- threshold: float = 0.0,
169
- on_unicode_error: str = "strict",
170
- use_strict_mode: bool = False
171
- ) -> List[Dict[str, Any]]:
172
- """
173
- Detect multiple potential languages and their probabilities in a given text.
174
- k controls the number of returned labels. A choice of 5, will return the 5 most probable labels. By default, this returns only the most likely label and probability. threshold filters the returned labels by a threshold on probability. A choice of 0.5 will return labels with at least 0.5 probability. k and threshold will be applied together to determine the returned labels.
175
- This function assumes to be given a single line of text. We split words on whitespace (space, newline, tab, vertical tab) and the control characters carriage return, formfeed, and the null character.
176
- If the model is not supervised, this function will throw a ValueError.
177
-
178
- :param text: The text for language detection
179
- :param low_memory: Whether to use a memory-efficient model
180
- :param model_download_proxy: Proxy for downloading the model
181
- :param k: Number of top language predictions to return
182
- :param threshold: Minimum score threshold for predictions
183
- :param on_unicode_error: Error handling for Unicode errors
184
- :param use_strict_mode: If enabled, strictly loads large model or raises error if it fails
185
- :return: A list of dictionaries, each containing a language and its confidence score
186
- :raises LanguageDetectionError: If detection fails
187
- """
188
- model = load_model(low_memory=low_memory, download_proxy=model_download_proxy, use_strict_mode=use_strict_mode)
189
- labels, scores = model.predict(text, k=k, threshold=threshold, on_unicode_error=on_unicode_error)
190
- results = []
191
- for label, score in zip(labels, scores):
192
- language_label = label.replace("__label__", '')
193
- confidence_score = min(float(score), 1.0)
194
- results.append({
195
- "lang": language_label,
196
- "score": confidence_score,
197
- })
198
- return sorted(results, key=lambda x: x['score'], reverse=True)
File without changes