lexiafrik 2.0.0__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.
- lexiafrik-2.0.0/LICENSE +21 -0
- lexiafrik-2.0.0/PKG-INFO +144 -0
- lexiafrik-2.0.0/README.md +109 -0
- lexiafrik-2.0.0/lexiafrik/__init__.py +54 -0
- lexiafrik-2.0.0/lexiafrik/__main__.py +5 -0
- lexiafrik-2.0.0/lexiafrik/auth.py +143 -0
- lexiafrik-2.0.0/lexiafrik/cli.py +82 -0
- lexiafrik-2.0.0/lexiafrik/client.py +413 -0
- lexiafrik-2.0.0/lexiafrik/exceptions.py +57 -0
- lexiafrik-2.0.0/lexiafrik/models.py +110 -0
- lexiafrik-2.0.0/lexiafrik.egg-info/PKG-INFO +144 -0
- lexiafrik-2.0.0/lexiafrik.egg-info/SOURCES.txt +17 -0
- lexiafrik-2.0.0/lexiafrik.egg-info/dependency_links.txt +1 -0
- lexiafrik-2.0.0/lexiafrik.egg-info/entry_points.txt +2 -0
- lexiafrik-2.0.0/lexiafrik.egg-info/requires.txt +1 -0
- lexiafrik-2.0.0/lexiafrik.egg-info/top_level.txt +1 -0
- lexiafrik-2.0.0/setup.cfg +4 -0
- lexiafrik-2.0.0/setup.py +33 -0
- lexiafrik-2.0.0/tests/test_client.py +290 -0
lexiafrik-2.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 XEL SOFT AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lexiafrik-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lexiafrik
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Python SDK and command line for LexiAfrik: speech-to-text for African languages
|
|
5
|
+
Home-page: https://lexiafrik.com
|
|
6
|
+
Author: XEL SOFT AI
|
|
7
|
+
Author-email: contact@xelsoftai.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Documentation, https://docs.lexiafrik.com
|
|
10
|
+
Project-URL: Source, https://github.com/xelsoft-ai/lexiafrik-python
|
|
11
|
+
Project-URL: Contact, https://lexiafrik.com/contact
|
|
12
|
+
Keywords: wolof,african-languages,speech-to-text,transcription,lexiafrik,asr
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: httpx>=0.24.0
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: keywords
|
|
29
|
+
Dynamic: license
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
Dynamic: project-url
|
|
32
|
+
Dynamic: requires-dist
|
|
33
|
+
Dynamic: requires-python
|
|
34
|
+
Dynamic: summary
|
|
35
|
+
|
|
36
|
+
# LexiAfrik Python SDK
|
|
37
|
+
|
|
38
|
+
Official Python SDK and command line for the [LexiAfrik API](https://docs.lexiafrik.com): speech-to-text for
|
|
39
|
+
Wolof and 30+ other African languages. Full documentation: **https://docs.lexiafrik.com**
|
|
40
|
+
|
|
41
|
+
🇫🇷 [Version française](https://github.com/xelsoft-ai/lexiafrik-python/blob/main/README.fr.md)
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install lexiafrik
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Python 3.8 or newer.
|
|
50
|
+
|
|
51
|
+
## Sign in from the terminal
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
lexiafrik login # prints a code, opens your browser, saves a key
|
|
55
|
+
lexiafrik whoami # the saved key and its plan
|
|
56
|
+
lexiafrik logout # deletes the credentials and revokes this terminal's key
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`lexiafrik login` needs no password and no key to copy: you sign in on lexiafrik.com, check the code shown
|
|
60
|
+
in the terminal and approve. An API key dedicated to this terminal is created on your account and saved in
|
|
61
|
+
`~/.lexiafrik/credentials.json` (readable by you only). You can revoke it at any time from
|
|
62
|
+
**lexiafrik.com > API keys**.
|
|
63
|
+
|
|
64
|
+
Key lookup order: the `api_key=` argument, the `LEXIAFRIK_API_KEY` environment variable, then the file saved
|
|
65
|
+
by `lexiafrik login`. On a server or in CI, create a key on the website and use the environment variable.
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from lexiafrik import LexiAfrikClient
|
|
71
|
+
|
|
72
|
+
client = LexiAfrikClient()
|
|
73
|
+
|
|
74
|
+
# Short file (50 MB maximum): immediate response
|
|
75
|
+
result = client.transcribe("speech.wav", language="wo", include_segments=True)
|
|
76
|
+
print(result.text)
|
|
77
|
+
for seg in result.segments:
|
|
78
|
+
print(seg) # [0.4s → 6.1s] First segment.
|
|
79
|
+
|
|
80
|
+
# Large file or video: direct upload, asynchronous job, wait for the result
|
|
81
|
+
result = client.transcribe_large("conference.mp4", language="wo",
|
|
82
|
+
on_progress=lambda n: print(n, "segments"))
|
|
83
|
+
|
|
84
|
+
# Direct link to an audio or video file
|
|
85
|
+
result = client.transcribe_url("https://example.com/recording.mp3")
|
|
86
|
+
|
|
87
|
+
# Languages, account, history
|
|
88
|
+
print([l.code for l in client.list_languages()])
|
|
89
|
+
print(client.get_account().available_minutes, "minutes available")
|
|
90
|
+
for t in client.list_transcriptions(page_size=5).items:
|
|
91
|
+
print(t.request_id, t.status)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Links to streaming platforms (YouTube, TikTok…) are not accepted: provide a direct link or upload the file.
|
|
95
|
+
|
|
96
|
+
## Error handling
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from lexiafrik import LexiAfrikClient, AuthenticationError, QuotaExceededError, LexiAfrikError
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
result = client.transcribe("audio.wav")
|
|
103
|
+
except AuthenticationError:
|
|
104
|
+
print("Invalid key: run `lexiafrik login` again")
|
|
105
|
+
except QuotaExceededError:
|
|
106
|
+
print("Monthly quota and credit exhausted")
|
|
107
|
+
except LexiAfrikError as e:
|
|
108
|
+
print(e) # English message; e.detail holds the server's own text, if any
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Exceptions: `AuthenticationError` (401/403), `QuotaExceededError` (402), `NotFoundError` (404),
|
|
112
|
+
`AudioTooLargeError` (413), `UnsupportedFormatError` (415), `TranscriptionError` (422),
|
|
113
|
+
`RateLimitError` (429), `APIError` (5xx).
|
|
114
|
+
|
|
115
|
+
## Environment variables
|
|
116
|
+
|
|
117
|
+
| Variable | Purpose |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `LEXIAFRIK_API_KEY` | API key (takes priority over the saved file). |
|
|
120
|
+
| `LEXIAFRIK_BASE_URL` | Another API address (tests). Default: `https://api.lexiafrik.com`. |
|
|
121
|
+
| `LEXIAFRIK_CREDENTIALS` | Another location for the credentials file. |
|
|
122
|
+
| `LEXIAFRIK_LANG` | Preferred language of server messages, sent as `Accept-Language` (default: `en`). |
|
|
123
|
+
|
|
124
|
+
## Migrating from version 1
|
|
125
|
+
|
|
126
|
+
SDK 2.0 follows the current API: a single result type (`TranscriptionResult`) for every mode, `get_usage()`
|
|
127
|
+
returns the account (plan, monthly quota, credit) rather than daily limits, and the `model`, `n_tokens`
|
|
128
|
+
and `rtf` fields of the result no longer exist.
|
|
129
|
+
|
|
130
|
+
## Tests
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python -m unittest discover -s tests -v
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
No external network and no real key needed.
|
|
137
|
+
|
|
138
|
+
## Get an API key
|
|
139
|
+
|
|
140
|
+
Create an account on [lexiafrik.com](https://lexiafrik.com), or run `lexiafrik login`.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# LexiAfrik Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK and command line for the [LexiAfrik API](https://docs.lexiafrik.com): speech-to-text for
|
|
4
|
+
Wolof and 30+ other African languages. Full documentation: **https://docs.lexiafrik.com**
|
|
5
|
+
|
|
6
|
+
🇫🇷 [Version française](https://github.com/xelsoft-ai/lexiafrik-python/blob/main/README.fr.md)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install lexiafrik
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Python 3.8 or newer.
|
|
15
|
+
|
|
16
|
+
## Sign in from the terminal
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
lexiafrik login # prints a code, opens your browser, saves a key
|
|
20
|
+
lexiafrik whoami # the saved key and its plan
|
|
21
|
+
lexiafrik logout # deletes the credentials and revokes this terminal's key
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`lexiafrik login` needs no password and no key to copy: you sign in on lexiafrik.com, check the code shown
|
|
25
|
+
in the terminal and approve. An API key dedicated to this terminal is created on your account and saved in
|
|
26
|
+
`~/.lexiafrik/credentials.json` (readable by you only). You can revoke it at any time from
|
|
27
|
+
**lexiafrik.com > API keys**.
|
|
28
|
+
|
|
29
|
+
Key lookup order: the `api_key=` argument, the `LEXIAFRIK_API_KEY` environment variable, then the file saved
|
|
30
|
+
by `lexiafrik login`. On a server or in CI, create a key on the website and use the environment variable.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from lexiafrik import LexiAfrikClient
|
|
36
|
+
|
|
37
|
+
client = LexiAfrikClient()
|
|
38
|
+
|
|
39
|
+
# Short file (50 MB maximum): immediate response
|
|
40
|
+
result = client.transcribe("speech.wav", language="wo", include_segments=True)
|
|
41
|
+
print(result.text)
|
|
42
|
+
for seg in result.segments:
|
|
43
|
+
print(seg) # [0.4s → 6.1s] First segment.
|
|
44
|
+
|
|
45
|
+
# Large file or video: direct upload, asynchronous job, wait for the result
|
|
46
|
+
result = client.transcribe_large("conference.mp4", language="wo",
|
|
47
|
+
on_progress=lambda n: print(n, "segments"))
|
|
48
|
+
|
|
49
|
+
# Direct link to an audio or video file
|
|
50
|
+
result = client.transcribe_url("https://example.com/recording.mp3")
|
|
51
|
+
|
|
52
|
+
# Languages, account, history
|
|
53
|
+
print([l.code for l in client.list_languages()])
|
|
54
|
+
print(client.get_account().available_minutes, "minutes available")
|
|
55
|
+
for t in client.list_transcriptions(page_size=5).items:
|
|
56
|
+
print(t.request_id, t.status)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Links to streaming platforms (YouTube, TikTok…) are not accepted: provide a direct link or upload the file.
|
|
60
|
+
|
|
61
|
+
## Error handling
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from lexiafrik import LexiAfrikClient, AuthenticationError, QuotaExceededError, LexiAfrikError
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
result = client.transcribe("audio.wav")
|
|
68
|
+
except AuthenticationError:
|
|
69
|
+
print("Invalid key: run `lexiafrik login` again")
|
|
70
|
+
except QuotaExceededError:
|
|
71
|
+
print("Monthly quota and credit exhausted")
|
|
72
|
+
except LexiAfrikError as e:
|
|
73
|
+
print(e) # English message; e.detail holds the server's own text, if any
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Exceptions: `AuthenticationError` (401/403), `QuotaExceededError` (402), `NotFoundError` (404),
|
|
77
|
+
`AudioTooLargeError` (413), `UnsupportedFormatError` (415), `TranscriptionError` (422),
|
|
78
|
+
`RateLimitError` (429), `APIError` (5xx).
|
|
79
|
+
|
|
80
|
+
## Environment variables
|
|
81
|
+
|
|
82
|
+
| Variable | Purpose |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `LEXIAFRIK_API_KEY` | API key (takes priority over the saved file). |
|
|
85
|
+
| `LEXIAFRIK_BASE_URL` | Another API address (tests). Default: `https://api.lexiafrik.com`. |
|
|
86
|
+
| `LEXIAFRIK_CREDENTIALS` | Another location for the credentials file. |
|
|
87
|
+
| `LEXIAFRIK_LANG` | Preferred language of server messages, sent as `Accept-Language` (default: `en`). |
|
|
88
|
+
|
|
89
|
+
## Migrating from version 1
|
|
90
|
+
|
|
91
|
+
SDK 2.0 follows the current API: a single result type (`TranscriptionResult`) for every mode, `get_usage()`
|
|
92
|
+
returns the account (plan, monthly quota, credit) rather than daily limits, and the `model`, `n_tokens`
|
|
93
|
+
and `rtf` fields of the result no longer exist.
|
|
94
|
+
|
|
95
|
+
## Tests
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python -m unittest discover -s tests -v
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
No external network and no real key needed.
|
|
102
|
+
|
|
103
|
+
## Get an API key
|
|
104
|
+
|
|
105
|
+
Create an account on [lexiafrik.com](https://lexiafrik.com), or run `lexiafrik login`.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LexiAfrik SDK — speech-to-text for Wolof and other African languages
|
|
3
|
+
|
|
4
|
+
Quick start:
|
|
5
|
+
from lexiafrik import LexiAfrikClient
|
|
6
|
+
|
|
7
|
+
client = LexiAfrikClient() # key: api_key=, LEXIAFRIK_API_KEY or `lexiafrik login`
|
|
8
|
+
result = client.transcribe("audio.wav", language="wo")
|
|
9
|
+
print(result.text)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .client import LexiAfrikClient
|
|
13
|
+
from .models import (
|
|
14
|
+
AccountInfo,
|
|
15
|
+
Language,
|
|
16
|
+
SegmentResult,
|
|
17
|
+
TranscriptionPage,
|
|
18
|
+
TranscriptionResult,
|
|
19
|
+
TranscriptionSummary,
|
|
20
|
+
URLTranscriptionResult,
|
|
21
|
+
)
|
|
22
|
+
from .exceptions import (
|
|
23
|
+
LexiAfrikError,
|
|
24
|
+
AuthenticationError,
|
|
25
|
+
QuotaExceededError,
|
|
26
|
+
RateLimitError,
|
|
27
|
+
NotFoundError,
|
|
28
|
+
AudioTooLargeError,
|
|
29
|
+
UnsupportedFormatError,
|
|
30
|
+
TranscriptionError,
|
|
31
|
+
APIError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__version__ = "2.0.0"
|
|
35
|
+
__author__ = "XEL SOFT AI"
|
|
36
|
+
__all__ = [
|
|
37
|
+
"LexiAfrikClient",
|
|
38
|
+
"TranscriptionResult",
|
|
39
|
+
"URLTranscriptionResult",
|
|
40
|
+
"SegmentResult",
|
|
41
|
+
"AccountInfo",
|
|
42
|
+
"Language",
|
|
43
|
+
"TranscriptionPage",
|
|
44
|
+
"TranscriptionSummary",
|
|
45
|
+
"LexiAfrikError",
|
|
46
|
+
"AuthenticationError",
|
|
47
|
+
"QuotaExceededError",
|
|
48
|
+
"RateLimitError",
|
|
49
|
+
"NotFoundError",
|
|
50
|
+
"AudioTooLargeError",
|
|
51
|
+
"UnsupportedFormatError",
|
|
52
|
+
"TranscriptionError",
|
|
53
|
+
"APIError",
|
|
54
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LexiAfrik SDK — Sign in a terminal without a password (device-code flow, RFC 8628).
|
|
3
|
+
|
|
4
|
+
from lexiafrik.auth import login
|
|
5
|
+
login() # prints a code, opens the browser, saves the key
|
|
6
|
+
|
|
7
|
+
The API key is created on your account when you approve, then saved in
|
|
8
|
+
``~/.lexiafrik/credentials.json`` (readable by you only). ``LexiAfrikClient()`` finds it
|
|
9
|
+
afterwards without any parameter. It stays revocable from lexiafrik.com > API keys.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import socket
|
|
16
|
+
import time
|
|
17
|
+
import webbrowser
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Callable, Optional
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
from .exceptions import AuthenticationError, LexiAfrikError
|
|
24
|
+
|
|
25
|
+
DEFAULT_BASE_URL = "https://api.lexiafrik.com"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def credentials_path() -> Path:
|
|
29
|
+
"""Credentials file (set LEXIAFRIK_CREDENTIALS to move it)."""
|
|
30
|
+
override = os.environ.get("LEXIAFRIK_CREDENTIALS")
|
|
31
|
+
return Path(override) if override else Path.home() / ".lexiafrik" / "credentials.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_credentials() -> Optional[dict]:
|
|
35
|
+
try:
|
|
36
|
+
data = json.loads(credentials_path().read_text(encoding="utf-8"))
|
|
37
|
+
except (OSError, ValueError):
|
|
38
|
+
return None
|
|
39
|
+
return data if isinstance(data, dict) and data.get("api_key") else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def save_credentials(data: dict) -> Path:
|
|
43
|
+
path = credentials_path()
|
|
44
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
try:
|
|
46
|
+
os.chmod(path.parent, 0o700)
|
|
47
|
+
except OSError:
|
|
48
|
+
pass
|
|
49
|
+
# Created directly as 0600: the key is never readable by other users.
|
|
50
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
51
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
52
|
+
json.dump(data, f, indent=2)
|
|
53
|
+
return path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def clear_credentials() -> bool:
|
|
57
|
+
try:
|
|
58
|
+
credentials_path().unlink()
|
|
59
|
+
return True
|
|
60
|
+
except FileNotFoundError:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def default_client_name() -> str:
|
|
65
|
+
try:
|
|
66
|
+
host = socket.gethostname()
|
|
67
|
+
except OSError:
|
|
68
|
+
host = "terminal"
|
|
69
|
+
return f"{host} ({platform.system() or 'CLI'})"[:60]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def login(
|
|
73
|
+
base_url: Optional[str] = None,
|
|
74
|
+
client_name: Optional[str] = None,
|
|
75
|
+
open_browser: bool = True,
|
|
76
|
+
echo: Callable[[str], None] = print,
|
|
77
|
+
save: bool = True,
|
|
78
|
+
timeout: float = 30.0,
|
|
79
|
+
) -> dict:
|
|
80
|
+
"""
|
|
81
|
+
Connect this terminal to your LexiAfrik account.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
``{"api_key", "key_hash", "prefix", "name", "base_url"}`` (saved to disk when ``save``).
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
AuthenticationError: sign-in denied in the browser.
|
|
88
|
+
LexiAfrikError: code expired or API unreachable.
|
|
89
|
+
"""
|
|
90
|
+
base = (base_url or os.environ.get("LEXIAFRIK_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
91
|
+
with httpx.Client(base_url=base, timeout=timeout) as http:
|
|
92
|
+
try:
|
|
93
|
+
r = http.post("/v1/auth/device", json={"client_name": client_name or default_client_name()})
|
|
94
|
+
except httpx.HTTPError as e:
|
|
95
|
+
raise LexiAfrikError(f"API unreachable ({base}): {e}") from e
|
|
96
|
+
if r.status_code == 429:
|
|
97
|
+
raise LexiAfrikError("Too many attempts. Try again in a few minutes.")
|
|
98
|
+
if r.status_code != 200:
|
|
99
|
+
raise LexiAfrikError(f"Could not start the sign-in (HTTP {r.status_code}).")
|
|
100
|
+
d = r.json()
|
|
101
|
+
|
|
102
|
+
url = d["verification_uri_complete"]
|
|
103
|
+
echo("")
|
|
104
|
+
echo("To connect this terminal, open:")
|
|
105
|
+
echo(f" {url}")
|
|
106
|
+
echo("")
|
|
107
|
+
echo(f"Confirmation code: {d['user_code']}")
|
|
108
|
+
echo("(only approve if you just ran this command)")
|
|
109
|
+
echo("")
|
|
110
|
+
if open_browser:
|
|
111
|
+
try:
|
|
112
|
+
webbrowser.open(url)
|
|
113
|
+
except Exception: # no browser (server, SSH): the URL stays displayed
|
|
114
|
+
pass
|
|
115
|
+
echo("Waiting for your approval…")
|
|
116
|
+
|
|
117
|
+
interval = max(1, int(d.get("interval", 5)))
|
|
118
|
+
deadline = time.monotonic() + int(d.get("expires_in", 600))
|
|
119
|
+
while time.monotonic() < deadline:
|
|
120
|
+
time.sleep(interval)
|
|
121
|
+
try:
|
|
122
|
+
r = http.post("/v1/auth/device/token", json={"device_code": d["device_code"]})
|
|
123
|
+
except httpx.HTTPError:
|
|
124
|
+
continue # transient network failure
|
|
125
|
+
if r.status_code == 200:
|
|
126
|
+
creds = {**r.json(), "base_url": base}
|
|
127
|
+
if save:
|
|
128
|
+
path = save_credentials(creds)
|
|
129
|
+
echo(f"Signed in. Key '{creds['name']}' saved to {path}")
|
|
130
|
+
return creds
|
|
131
|
+
is_json = r.headers.get("content-type", "").startswith("application/json")
|
|
132
|
+
err = (r.json() if is_json else {}).get("error")
|
|
133
|
+
if err == "authorization_pending":
|
|
134
|
+
continue
|
|
135
|
+
if err == "slow_down":
|
|
136
|
+
interval += 5
|
|
137
|
+
continue
|
|
138
|
+
if err == "access_denied":
|
|
139
|
+
raise AuthenticationError("Sign-in denied in the browser.")
|
|
140
|
+
if err == "expired_token":
|
|
141
|
+
raise LexiAfrikError("The code has expired. Run `lexiafrik login` again.")
|
|
142
|
+
raise LexiAfrikError(f"Sign-in failed ({err or r.status_code}).")
|
|
143
|
+
raise LexiAfrikError("The code has expired. Run `lexiafrik login` again.")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""LexiAfrik SDK — command line: ``lexiafrik login | logout | whoami``."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .auth import DEFAULT_BASE_URL, clear_credentials, credentials_path, load_credentials, login
|
|
10
|
+
from .exceptions import LexiAfrikError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _login(args) -> int:
|
|
14
|
+
login(base_url=args.base_url, client_name=args.name, open_browser=not args.no_browser)
|
|
15
|
+
return 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _logout(args) -> int:
|
|
19
|
+
creds = load_credentials()
|
|
20
|
+
if not creds:
|
|
21
|
+
print("No saved credentials.")
|
|
22
|
+
return 0
|
|
23
|
+
if not args.keep_key and creds.get("key_hash"):
|
|
24
|
+
# Revoke this terminal's key on the LexiAfrik side (best effort: the file is deleted regardless).
|
|
25
|
+
try:
|
|
26
|
+
r = httpx.delete(f"{creds.get('base_url', DEFAULT_BASE_URL)}/v1/keys/{creds['key_hash']}",
|
|
27
|
+
headers={"X-API-Key": creds["api_key"]}, timeout=15)
|
|
28
|
+
print("Key revoked." if r.status_code < 300 else
|
|
29
|
+
"The key could not be revoked; revoke it from lexiafrik.com > API keys.")
|
|
30
|
+
except httpx.HTTPError:
|
|
31
|
+
print("API unreachable: revoke the key from lexiafrik.com > API keys.")
|
|
32
|
+
clear_credentials()
|
|
33
|
+
print(f"Credentials deleted ({credentials_path()}).")
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _whoami(_args) -> int:
|
|
38
|
+
creds = load_credentials()
|
|
39
|
+
if not creds:
|
|
40
|
+
print("Not signed in. Run `lexiafrik login`.")
|
|
41
|
+
return 1
|
|
42
|
+
r = httpx.get(f"{creds.get('base_url', DEFAULT_BASE_URL)}/v1/whoami",
|
|
43
|
+
headers={"X-API-Key": creds["api_key"]}, timeout=15)
|
|
44
|
+
if r.status_code != 200:
|
|
45
|
+
print("Invalid or revoked key. Run `lexiafrik login` again.")
|
|
46
|
+
return 1
|
|
47
|
+
who = r.json()
|
|
48
|
+
print(f"Key: {creds.get('name', '')} ({creds.get('prefix', '')}…) Plan: {who.get('tier', '?')}")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main(argv=None) -> int:
|
|
53
|
+
parser = argparse.ArgumentParser(prog="lexiafrik", description="LexiAfrik - speech-to-text for African languages")
|
|
54
|
+
parser.add_argument("--version", action="version", version=f"lexiafrik {__version__}")
|
|
55
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
56
|
+
|
|
57
|
+
p = sub.add_parser("login", help="connect this terminal to your account (no password)")
|
|
58
|
+
p.add_argument("--no-browser", action="store_true", help="print the link without opening the browser")
|
|
59
|
+
p.add_argument("--name", help="name of this terminal (shown when you approve it)")
|
|
60
|
+
p.add_argument("--base-url", help=f"API URL (default: {DEFAULT_BASE_URL})")
|
|
61
|
+
p.set_defaults(func=_login)
|
|
62
|
+
|
|
63
|
+
p = sub.add_parser("logout", help="delete saved credentials and revoke this terminal's key")
|
|
64
|
+
p.add_argument("--keep-key", action="store_true", help="do not revoke the key on the LexiAfrik side")
|
|
65
|
+
p.set_defaults(func=_logout)
|
|
66
|
+
|
|
67
|
+
p = sub.add_parser("whoami", help="show the saved key and its plan")
|
|
68
|
+
p.set_defaults(func=_whoami)
|
|
69
|
+
|
|
70
|
+
args = parser.parse_args(argv)
|
|
71
|
+
try:
|
|
72
|
+
return args.func(args)
|
|
73
|
+
except LexiAfrikError as e:
|
|
74
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
except KeyboardInterrupt:
|
|
77
|
+
print("\nInterrupted.", file=sys.stderr)
|
|
78
|
+
return 130
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
sys.exit(main())
|