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,8 @@
1
+ models/**
2
+ !models/voices/.gitkeep
3
+ __pycache__/
4
+ *.pyc
5
+ .env
6
+ .env.bak
7
+ .pytest_cache/
8
+ .venv/
@@ -0,0 +1,3 @@
1
+ [pypi]
2
+ username = __token__
3
+ password = pypi-AgEIcHlwaS5vcmcCJGNhOTQ0ODc5LTU1YTktNDI3YS1iYmI1LTU0OWNlNGMxMGY4YwACKlszLCI0MzZhY2YxMy03ZWIzLTQwM2EtOWExZS1iYmFhNGU2NzBiYzgiXQAABiB7p3KXJvMb-AdPLN9lKB8TRlmrHgE0KNZ8RcsbHdrpiQ
@@ -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,2 @@
1
+ from .client import AsyncNavaClient, NavaClient, NavaServiceError, SpeechResult, TranscriptionResult
2
+ __all__ = ["AsyncNavaClient", "NavaClient", "NavaServiceError", "SpeechResult", "TranscriptionResult"]
@@ -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"]