nava-fa-speech 1.0.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.
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nava-fa-speech
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Python SDK for Nava offline Persian speech services
|
|
5
|
+
Project-URL: Homepage, https://github.com/mhosseinifar8/nava
|
|
6
|
+
Project-URL: Repository, https://github.com/mhosseinifar8/nava
|
|
7
|
+
Project-URL: Documentation, https://github.com/mhosseinifar8/nava/tree/main/docs
|
|
8
|
+
Project-URL: Issues, https://github.com/mhosseinifar8/nava/issues
|
|
9
|
+
Keywords: asr,farsi,offline,persian,speech,speech-to-text,stt,text-to-speech,tts,whisper
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: Persian
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx<1,>=0.27
|
|
22
|
+
Requires-Dist: websockets<16,>=15
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Nava — Python SDK for Offline Persian Speech
|
|
26
|
+
|
|
27
|
+
**Nava** (نوا — "melody" in Persian) is the official Python client for the
|
|
28
|
+
[Nava speech platform](https://github.com/mhosseinifar8/nava) — an **offline, CPU-only Persian
|
|
29
|
+
speech service**:
|
|
30
|
+
|
|
31
|
+
- 🗣️ **Speech-to-Text** — Whisper `large-v3` (faster-whisper) tuned for Persian,
|
|
32
|
+
with standard orthography (ZWNJ, punctuation), VAD, and hot-word support.
|
|
33
|
+
- 🔊 **Text-to-Speech** — three engines: `pocket` (voice-cloning style prompts),
|
|
34
|
+
`mana` and `piper` (Piper ONNX voices), all running on CPU.
|
|
35
|
+
- 🔒 **Fully offline** — your audio and text never leave your machine. No cloud,
|
|
36
|
+
no API keys, no telemetry.
|
|
37
|
+
|
|
38
|
+
This SDK wraps both services behind a tiny, typed Python API:
|
|
39
|
+
|
|
40
|
+
| Client | Transport | Methods |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `NavaClient` | REST (sync) | `transcribe`, `synthesize` |
|
|
43
|
+
| `AsyncNavaClient` | REST + WebSocket (async) | `transcribe`, `synthesize`, `transcribe_ws`, `stream_speech_ws` |
|
|
44
|
+
|
|
45
|
+
- `transcribe_ws` streams your audio in frames (long recordings work without
|
|
46
|
+
hitting per-frame size limits) and reuses the connection protocol efficiently.
|
|
47
|
+
- `stream_speech_ws` yields PCM chunks **as they are synthesized** — start
|
|
48
|
+
playing audio before the whole clip is ready.
|
|
49
|
+
- All failures raise a single, clear `NavaServiceError`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
- Python **3.10+**
|
|
56
|
+
- A running Nava server (see [Start the server](#start-the-server)) — the SDK
|
|
57
|
+
defaults to `http://127.0.0.1:8001` (STT) and `http://127.0.0.1:8002` (TTS).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Start the server
|
|
62
|
+
|
|
63
|
+
Nava is self-hosted from [its repository](https://github.com/mhosseinifar8/nava) — clone it, then:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 1. Download all models (~10 GB, one time — needs internet)
|
|
67
|
+
./scripts/download_models.sh
|
|
68
|
+
|
|
69
|
+
# 2. Generate a .env tuned to THIS machine (cores/RAM auto-detected)
|
|
70
|
+
./scripts/configure_infra.sh auto
|
|
71
|
+
|
|
72
|
+
# 3. Build and run both services (offline from here on)
|
|
73
|
+
docker compose up --build -d
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Verify:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
curl -s http://127.0.0.1:8001/api/v1/health
|
|
80
|
+
# {"status":"ok","service":"nava-stt"}
|
|
81
|
+
curl -s http://127.0.0.1:8002/api/v1/health
|
|
82
|
+
# {"status":"ok","service":"nava-tts","engines":["pocket","mana","piper"]}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> Models are downloaded into `./models` and mounted read-only into the
|
|
86
|
+
> containers. `configure_infra.sh` picks `low` / `medium` / `high` profiles
|
|
87
|
+
> automatically (or force one: `./scripts/configure_infra.sh low`).
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Installation
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install nava-speech
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or from source:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install -e ./nava_sdk
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> The PyPI package is **`nava-speech`**, but you import it as `nava`:
|
|
104
|
+
> `from nava import NavaClient` (same convention as `pillow` → `PIL`).
|
|
105
|
+
|
|
106
|
+
Dependencies: `httpx` and `websockets` (installed automatically).
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Quick start
|
|
111
|
+
|
|
112
|
+
### Speech-to-text
|
|
113
|
+
|
|
114
|
+
Input must be raw PCM: **signed 16-bit little-endian, mono, 16 000 Hz**.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from nava import NavaClient
|
|
118
|
+
|
|
119
|
+
with NavaClient() as nava:
|
|
120
|
+
with open("speech.pcm", "rb") as f:
|
|
121
|
+
result = nava.transcribe(f.read(), hotwords="نوا")
|
|
122
|
+
|
|
123
|
+
print(result.text) # متن تشخیص داده شده
|
|
124
|
+
print(result.language_prob) # 0.99
|
|
125
|
+
for segment in result.segments:
|
|
126
|
+
print(segment["start"], segment["end"], segment["text"])
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Convert any audio file to the required format with ffmpeg:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
ffmpeg -i input.mp3 -ar 16000 -ac 1 -f s16le speech.pcm
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Text-to-speech
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from nava import NavaClient
|
|
139
|
+
|
|
140
|
+
with NavaClient() as nava:
|
|
141
|
+
speech = nava.synthesize("سلام دنیا", engine="piper", speed=1.1)
|
|
142
|
+
|
|
143
|
+
print(speech.sample_rate) # read this — it differs per engine
|
|
144
|
+
with open("out.pcm", "wb") as f:
|
|
145
|
+
f.write(speech.pcm) # raw PCM, signed 16-bit LE, mono
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Play the result (use the engine's sample rate):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
ffplay -f s16le -ar 22050 -ch_layout mono out.pcm
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Engines**
|
|
155
|
+
|
|
156
|
+
| Engine | Voice | Speed | Notes |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `pocket` (default) | any wav prompt in `models/voices/` (`voice_prompt=`) | — | expressive, voice-cloning style |
|
|
159
|
+
| `mana` | fixed | 0.5–2.0 | Piper ONNX |
|
|
160
|
+
| `piper` | fixed | 0.5–2.0 | fastest |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Async & streaming
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import asyncio
|
|
168
|
+
from nava import AsyncNavaClient
|
|
169
|
+
|
|
170
|
+
async def main():
|
|
171
|
+
async with AsyncNavaClient() as nava:
|
|
172
|
+
# REST
|
|
173
|
+
result = await nava.transcribe(pcm_bytes)
|
|
174
|
+
speech = await nava.synthesize("سلام", engine="mana")
|
|
175
|
+
|
|
176
|
+
# WebSocket STT — long audio is auto-chunked into 1 MB frames
|
|
177
|
+
result = await nava.transcribe_ws(pcm_bytes, hotwords="نوا")
|
|
178
|
+
|
|
179
|
+
# WebSocket TTS — play chunks as they are synthesized
|
|
180
|
+
async for chunk in nava.stream_speech_ws("این یک آزمایش است", engine="pocket"):
|
|
181
|
+
play(chunk.pcm) # chunk.sample_rate for your audio sink
|
|
182
|
+
|
|
183
|
+
asyncio.run(main())
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Error handling
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from nava import NavaClient, NavaServiceError
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
with NavaClient() as nava:
|
|
195
|
+
nava.transcribe(b"\x00" * 10) # too short
|
|
196
|
+
except NavaServiceError as exc:
|
|
197
|
+
print(exc)
|
|
198
|
+
# Nava returned HTTP 422: audio is shorter than 100 ms at 16 kHz
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## API reference
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
NavaClient(stt_url="http://127.0.0.1:8001",
|
|
207
|
+
tts_url="http://127.0.0.1:8002", *, timeout=120.0)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| Method | Returns | Description |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `transcribe(pcm, *, hotwords=None, initial_prompt=None)` | `TranscriptionResult(text, segments, language_prob)` | REST STT |
|
|
213
|
+
| `synthesize(text, *, engine="pocket", voice_prompt="default", speed=1.0)` | `SpeechResult(pcm, sample_rate, engine)` | REST TTS |
|
|
214
|
+
| `AsyncNavaClient.transcribe_ws(pcm, *, hotwords=None, initial_prompt=None)` | `TranscriptionResult` | WebSocket STT (frame-chunked) |
|
|
215
|
+
| `AsyncNavaClient.stream_speech_ws(text, *, engine, voice_prompt, speed)` | async iterator of `SpeechResult` | WebSocket TTS streaming |
|
|
216
|
+
|
|
217
|
+
Both clients are context managers (`with` / `async with`) and share one pooled
|
|
218
|
+
HTTP connection across calls. Point them at remote hosts by passing
|
|
219
|
+
`stt_url=` / `tts_url=`.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Links
|
|
224
|
+
|
|
225
|
+
- [Architecture guide](https://github.com/mhosseinifar8/nava/blob/main/docs/ARCHITECTURE.md) ·
|
|
226
|
+
[User manual](https://github.com/mhosseinifar8/nava/blob/main/docs/USER_MANUAL.md)
|
|
227
|
+
- Report issues on the [project tracker](https://github.com/mhosseinifar8/nava/issues).
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Nava — Python SDK for Offline Persian Speech
|
|
2
|
+
|
|
3
|
+
**Nava** (نوا — "melody" in Persian) is the official Python client for the
|
|
4
|
+
[Nava speech platform](https://github.com/mhosseinifar8/nava) — an **offline, CPU-only Persian
|
|
5
|
+
speech service**:
|
|
6
|
+
|
|
7
|
+
- 🗣️ **Speech-to-Text** — Whisper `large-v3` (faster-whisper) tuned for Persian,
|
|
8
|
+
with standard orthography (ZWNJ, punctuation), VAD, and hot-word support.
|
|
9
|
+
- 🔊 **Text-to-Speech** — three engines: `pocket` (voice-cloning style prompts),
|
|
10
|
+
`mana` and `piper` (Piper ONNX voices), all running on CPU.
|
|
11
|
+
- 🔒 **Fully offline** — your audio and text never leave your machine. No cloud,
|
|
12
|
+
no API keys, no telemetry.
|
|
13
|
+
|
|
14
|
+
This SDK wraps both services behind a tiny, typed Python API:
|
|
15
|
+
|
|
16
|
+
| Client | Transport | Methods |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `NavaClient` | REST (sync) | `transcribe`, `synthesize` |
|
|
19
|
+
| `AsyncNavaClient` | REST + WebSocket (async) | `transcribe`, `synthesize`, `transcribe_ws`, `stream_speech_ws` |
|
|
20
|
+
|
|
21
|
+
- `transcribe_ws` streams your audio in frames (long recordings work without
|
|
22
|
+
hitting per-frame size limits) and reuses the connection protocol efficiently.
|
|
23
|
+
- `stream_speech_ws` yields PCM chunks **as they are synthesized** — start
|
|
24
|
+
playing audio before the whole clip is ready.
|
|
25
|
+
- All failures raise a single, clear `NavaServiceError`.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python **3.10+**
|
|
32
|
+
- A running Nava server (see [Start the server](#start-the-server)) — the SDK
|
|
33
|
+
defaults to `http://127.0.0.1:8001` (STT) and `http://127.0.0.1:8002` (TTS).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Start the server
|
|
38
|
+
|
|
39
|
+
Nava is self-hosted from [its repository](https://github.com/mhosseinifar8/nava) — clone it, then:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# 1. Download all models (~10 GB, one time — needs internet)
|
|
43
|
+
./scripts/download_models.sh
|
|
44
|
+
|
|
45
|
+
# 2. Generate a .env tuned to THIS machine (cores/RAM auto-detected)
|
|
46
|
+
./scripts/configure_infra.sh auto
|
|
47
|
+
|
|
48
|
+
# 3. Build and run both services (offline from here on)
|
|
49
|
+
docker compose up --build -d
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Verify:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
curl -s http://127.0.0.1:8001/api/v1/health
|
|
56
|
+
# {"status":"ok","service":"nava-stt"}
|
|
57
|
+
curl -s http://127.0.0.1:8002/api/v1/health
|
|
58
|
+
# {"status":"ok","service":"nava-tts","engines":["pocket","mana","piper"]}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> Models are downloaded into `./models` and mounted read-only into the
|
|
62
|
+
> containers. `configure_infra.sh` picks `low` / `medium` / `high` profiles
|
|
63
|
+
> automatically (or force one: `./scripts/configure_infra.sh low`).
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install nava-speech
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or from source:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e ./nava_sdk
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> The PyPI package is **`nava-speech`**, but you import it as `nava`:
|
|
80
|
+
> `from nava import NavaClient` (same convention as `pillow` → `PIL`).
|
|
81
|
+
|
|
82
|
+
Dependencies: `httpx` and `websockets` (installed automatically).
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Quick start
|
|
87
|
+
|
|
88
|
+
### Speech-to-text
|
|
89
|
+
|
|
90
|
+
Input must be raw PCM: **signed 16-bit little-endian, mono, 16 000 Hz**.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from nava import NavaClient
|
|
94
|
+
|
|
95
|
+
with NavaClient() as nava:
|
|
96
|
+
with open("speech.pcm", "rb") as f:
|
|
97
|
+
result = nava.transcribe(f.read(), hotwords="نوا")
|
|
98
|
+
|
|
99
|
+
print(result.text) # متن تشخیص داده شده
|
|
100
|
+
print(result.language_prob) # 0.99
|
|
101
|
+
for segment in result.segments:
|
|
102
|
+
print(segment["start"], segment["end"], segment["text"])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Convert any audio file to the required format with ffmpeg:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
ffmpeg -i input.mp3 -ar 16000 -ac 1 -f s16le speech.pcm
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Text-to-speech
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from nava import NavaClient
|
|
115
|
+
|
|
116
|
+
with NavaClient() as nava:
|
|
117
|
+
speech = nava.synthesize("سلام دنیا", engine="piper", speed=1.1)
|
|
118
|
+
|
|
119
|
+
print(speech.sample_rate) # read this — it differs per engine
|
|
120
|
+
with open("out.pcm", "wb") as f:
|
|
121
|
+
f.write(speech.pcm) # raw PCM, signed 16-bit LE, mono
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Play the result (use the engine's sample rate):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
ffplay -f s16le -ar 22050 -ch_layout mono out.pcm
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Engines**
|
|
131
|
+
|
|
132
|
+
| Engine | Voice | Speed | Notes |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| `pocket` (default) | any wav prompt in `models/voices/` (`voice_prompt=`) | — | expressive, voice-cloning style |
|
|
135
|
+
| `mana` | fixed | 0.5–2.0 | Piper ONNX |
|
|
136
|
+
| `piper` | fixed | 0.5–2.0 | fastest |
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Async & streaming
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
import asyncio
|
|
144
|
+
from nava import AsyncNavaClient
|
|
145
|
+
|
|
146
|
+
async def main():
|
|
147
|
+
async with AsyncNavaClient() as nava:
|
|
148
|
+
# REST
|
|
149
|
+
result = await nava.transcribe(pcm_bytes)
|
|
150
|
+
speech = await nava.synthesize("سلام", engine="mana")
|
|
151
|
+
|
|
152
|
+
# WebSocket STT — long audio is auto-chunked into 1 MB frames
|
|
153
|
+
result = await nava.transcribe_ws(pcm_bytes, hotwords="نوا")
|
|
154
|
+
|
|
155
|
+
# WebSocket TTS — play chunks as they are synthesized
|
|
156
|
+
async for chunk in nava.stream_speech_ws("این یک آزمایش است", engine="pocket"):
|
|
157
|
+
play(chunk.pcm) # chunk.sample_rate for your audio sink
|
|
158
|
+
|
|
159
|
+
asyncio.run(main())
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Error handling
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from nava import NavaClient, NavaServiceError
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
with NavaClient() as nava:
|
|
171
|
+
nava.transcribe(b"\x00" * 10) # too short
|
|
172
|
+
except NavaServiceError as exc:
|
|
173
|
+
print(exc)
|
|
174
|
+
# Nava returned HTTP 422: audio is shorter than 100 ms at 16 kHz
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## API reference
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
NavaClient(stt_url="http://127.0.0.1:8001",
|
|
183
|
+
tts_url="http://127.0.0.1:8002", *, timeout=120.0)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Method | Returns | Description |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `transcribe(pcm, *, hotwords=None, initial_prompt=None)` | `TranscriptionResult(text, segments, language_prob)` | REST STT |
|
|
189
|
+
| `synthesize(text, *, engine="pocket", voice_prompt="default", speed=1.0)` | `SpeechResult(pcm, sample_rate, engine)` | REST TTS |
|
|
190
|
+
| `AsyncNavaClient.transcribe_ws(pcm, *, hotwords=None, initial_prompt=None)` | `TranscriptionResult` | WebSocket STT (frame-chunked) |
|
|
191
|
+
| `AsyncNavaClient.stream_speech_ws(text, *, engine, voice_prompt, speed)` | async iterator of `SpeechResult` | WebSocket TTS streaming |
|
|
192
|
+
|
|
193
|
+
Both clients are context managers (`with` / `async with`) and share one pooled
|
|
194
|
+
HTTP connection across calls. Point them at remote hosts by passing
|
|
195
|
+
`stt_url=` / `tts_url=`.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Links
|
|
200
|
+
|
|
201
|
+
- [Architecture guide](https://github.com/mhosseinifar8/nava/blob/main/docs/ARCHITECTURE.md) ·
|
|
202
|
+
[User manual](https://github.com/mhosseinifar8/nava/blob/main/docs/USER_MANUAL.md)
|
|
203
|
+
- Report issues on the [project tracker](https://github.com/mhosseinifar8/nava/issues).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from websockets.asyncio.client import connect
|
|
9
|
+
from websockets.exceptions import ConnectionClosed
|
|
10
|
+
|
|
11
|
+
WS_FRAME_CHUNK = 1_000_000 # stay well under server per-frame limits
|
|
12
|
+
|
|
13
|
+
class NavaServiceError(RuntimeError): pass
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class TranscriptionResult:
|
|
16
|
+
text: str
|
|
17
|
+
segments: list[dict]
|
|
18
|
+
language_prob: float
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class SpeechResult:
|
|
21
|
+
pcm: bytes
|
|
22
|
+
sample_rate: int
|
|
23
|
+
engine: str
|
|
24
|
+
def check(response: httpx.Response) -> None:
|
|
25
|
+
if response.is_success: return
|
|
26
|
+
try: detail=response.json().get("detail",response.text)
|
|
27
|
+
except ValueError: detail=response.text
|
|
28
|
+
raise NavaServiceError(f"Nava returned HTTP {response.status_code}: {detail}")
|
|
29
|
+
def ws_url(url: str, path: str) -> str:
|
|
30
|
+
return ("wss://"+url[8:] if url.startswith("https://") else "ws://"+url[7:] if url.startswith("http://") else url)+path
|
|
31
|
+
async def _recv_or_raise(socket) -> str | bytes:
|
|
32
|
+
try:
|
|
33
|
+
return await socket.recv()
|
|
34
|
+
except ConnectionClosed as exc:
|
|
35
|
+
raise NavaServiceError(f"connection closed before the result arrived: {exc}") from exc
|
|
36
|
+
async def _send_chunked(socket, payload: bytes) -> None:
|
|
37
|
+
for i in range(0, len(payload), WS_FRAME_CHUNK):
|
|
38
|
+
await socket.send(payload[i:i + WS_FRAME_CHUNK])
|
|
39
|
+
|
|
40
|
+
class NavaClient:
|
|
41
|
+
def __init__(self,stt_url="http://127.0.0.1:8001",tts_url="http://127.0.0.1:8002",*,timeout=120.0): self.stt_url=stt_url.rstrip("/"); self.tts_url=tts_url.rstrip("/"); self.http=httpx.Client(timeout=timeout)
|
|
42
|
+
def __enter__(self): return self
|
|
43
|
+
def __exit__(self,*_): self.close()
|
|
44
|
+
def close(self): self.http.close()
|
|
45
|
+
def transcribe(self,pcm:bytes,*,hotwords:str|None=None,initial_prompt:str|None=None)->TranscriptionResult:
|
|
46
|
+
r=self.http.post(self.stt_url+"/api/v1/transcriptions",content=pcm,headers={"Content-Type":"application/octet-stream"},params={k:v for k,v in {"hotwords":hotwords,"initial_prompt":initial_prompt}.items() if v}); check(r); b=r.json(); return TranscriptionResult(b["text"],b["segments"],b["language_prob"])
|
|
47
|
+
def synthesize(self,text:str,*,engine="pocket",voice_prompt="default",speed=1.0)->SpeechResult:
|
|
48
|
+
r=self.http.post(self.tts_url+"/api/v1/speech",json={"text":text,"engine":engine,"voice_prompt":voice_prompt,"speed":speed}); check(r); return SpeechResult(r.content,int(r.headers["X-Nava-Sample-Rate"]),r.headers["X-Nava-Engine"])
|
|
49
|
+
class AsyncNavaClient:
|
|
50
|
+
def __init__(self,stt_url="http://127.0.0.1:8001",tts_url="http://127.0.0.1:8002",*,timeout=120.0): self.stt_url=stt_url.rstrip("/"); self.tts_url=tts_url.rstrip("/"); self.http=httpx.AsyncClient(timeout=timeout)
|
|
51
|
+
async def __aenter__(self): return self
|
|
52
|
+
async def __aexit__(self,*_): await self.aclose()
|
|
53
|
+
async def aclose(self): await self.http.aclose()
|
|
54
|
+
async def transcribe(self,pcm:bytes,*,hotwords:str|None=None,initial_prompt:str|None=None)->TranscriptionResult:
|
|
55
|
+
r=await self.http.post(self.stt_url+"/api/v1/transcriptions",content=pcm,headers={"Content-Type":"application/octet-stream"},params={k:v for k,v in {"hotwords":hotwords,"initial_prompt":initial_prompt}.items() if v}); check(r); b=r.json(); return TranscriptionResult(b["text"],b["segments"],b["language_prob"])
|
|
56
|
+
async def synthesize(self,text:str,*,engine="pocket",voice_prompt="default",speed=1.0)->SpeechResult:
|
|
57
|
+
r=await self.http.post(self.tts_url+"/api/v1/speech",json={"text":text,"engine":engine,"voice_prompt":voice_prompt,"speed":speed}); check(r); return SpeechResult(r.content,int(r.headers["X-Nava-Sample-Rate"]),r.headers["X-Nava-Engine"])
|
|
58
|
+
async def transcribe_ws(self,pcm:bytes,*,hotwords:str|None=None,initial_prompt:str|None=None)->TranscriptionResult:
|
|
59
|
+
async with connect(ws_url(self.stt_url,"/ws/stt"),max_size=None) as socket:
|
|
60
|
+
config={k:v for k,v in {"hotwords":hotwords,"initial_prompt":initial_prompt}.items() if v}
|
|
61
|
+
if config: await socket.send(json.dumps(config,ensure_ascii=False))
|
|
62
|
+
await _send_chunked(socket, pcm)
|
|
63
|
+
await socket.send('{"action":"eos"}')
|
|
64
|
+
while True:
|
|
65
|
+
frame=await _recv_or_raise(socket)
|
|
66
|
+
if isinstance(frame,bytes): continue
|
|
67
|
+
body=json.loads(frame)
|
|
68
|
+
if body.get("status")=="error": raise NavaServiceError(body["error"])
|
|
69
|
+
if "text" in body: return TranscriptionResult(body["text"],body["segments"],body["language_prob"])
|
|
70
|
+
async def stream_speech_ws(self,text:str,*,engine="pocket",voice_prompt="default",speed=1.0)->AsyncIterator[SpeechResult]:
|
|
71
|
+
async with connect(ws_url(self.tts_url,"/ws/tts"),max_size=None) as socket:
|
|
72
|
+
await socket.send(json.dumps({"text":text,"engine":engine,"voice_prompt":voice_prompt,"speed":speed},ensure_ascii=False)); rate=None
|
|
73
|
+
while True:
|
|
74
|
+
frame=await _recv_or_raise(socket)
|
|
75
|
+
if isinstance(frame,bytes):
|
|
76
|
+
if rate is None: raise NavaServiceError("TTS sent audio before metadata")
|
|
77
|
+
yield SpeechResult(frame,rate,engine); continue
|
|
78
|
+
body=json.loads(frame)
|
|
79
|
+
if body.get("status")=="error": raise NavaServiceError(body["error"])
|
|
80
|
+
if body.get("status")=="streaming": rate=int(body["sample_rate"]); engine=body["engine"]
|
|
81
|
+
if body.get("status")=="completed": return
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nava-fa-speech"
|
|
7
|
+
version = "1.0.4"
|
|
8
|
+
description = "Python SDK for Nava offline Persian speech services"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
keywords = ["persian", "farsi", "speech", "tts", "stt", "asr", "whisper", "offline", "speech-to-text", "text-to-speech"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Natural Language :: Persian",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["httpx>=0.27,<1", "websockets>=15,<16"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/mhosseinifar8/nava"
|
|
28
|
+
Repository = "https://github.com/mhosseinifar8/nava"
|
|
29
|
+
Documentation = "https://github.com/mhosseinifar8/nava/tree/main/docs"
|
|
30
|
+
Issues = "https://github.com/mhosseinifar8/nava/issues"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
# PyPI distribution name is "nava-speech"; the importable package stays "nava"
|
|
34
|
+
packages = ["nava"]
|