kotoba-sdk 0.1.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.
Files changed (44) hide show
  1. kotoba_sdk-0.1.0/.claude/settings.local.json +7 -0
  2. kotoba_sdk-0.1.0/.gitignore +11 -0
  3. kotoba_sdk-0.1.0/LICENSE +21 -0
  4. kotoba_sdk-0.1.0/PKG-INFO +321 -0
  5. kotoba_sdk-0.1.0/README.md +266 -0
  6. kotoba_sdk-0.1.0/docs/quickstart.md +140 -0
  7. kotoba_sdk-0.1.0/examples/asr_rest_async.py +38 -0
  8. kotoba_sdk-0.1.0/examples/asr_rest_sync.py +37 -0
  9. kotoba_sdk-0.1.0/examples/asr_stream_async.py +107 -0
  10. kotoba_sdk-0.1.0/examples/audio/en/example.mp3 +0 -0
  11. kotoba_sdk-0.1.0/examples/audio/ja/example.mp3 +0 -0
  12. kotoba_sdk-0.1.0/examples/s2st_mic_async.py +131 -0
  13. kotoba_sdk-0.1.0/examples/s2st_stream_async.py +77 -0
  14. kotoba_sdk-0.1.0/examples/tts_stream_async.py +66 -0
  15. kotoba_sdk-0.1.0/examples/tts_synthesize_sync.py +24 -0
  16. kotoba_sdk-0.1.0/out_ja-woman-eriko.wav +0 -0
  17. kotoba_sdk-0.1.0/out_ja-woman-f04-me.wav +0 -0
  18. kotoba_sdk-0.1.0/pyproject.toml +62 -0
  19. kotoba_sdk-0.1.0/s2st_ja2en.wav +0 -0
  20. kotoba_sdk-0.1.0/sample_24k.wav +0 -0
  21. kotoba_sdk-0.1.0/scratch_asr_bench.py +34 -0
  22. kotoba_sdk-0.1.0/scratch_tts_test.py +36 -0
  23. kotoba_sdk-0.1.0/src/kotoba/__init__.py +65 -0
  24. kotoba_sdk-0.1.0/src/kotoba/_http.py +258 -0
  25. kotoba_sdk-0.1.0/src/kotoba/_version.py +1 -0
  26. kotoba_sdk-0.1.0/src/kotoba/_ws_asr.py +235 -0
  27. kotoba_sdk-0.1.0/src/kotoba/_ws_base.py +262 -0
  28. kotoba_sdk-0.1.0/src/kotoba/_ws_s2st.py +180 -0
  29. kotoba_sdk-0.1.0/src/kotoba/_ws_tts.py +243 -0
  30. kotoba_sdk-0.1.0/src/kotoba/asr.py +449 -0
  31. kotoba_sdk-0.1.0/src/kotoba/audio.py +148 -0
  32. kotoba_sdk-0.1.0/src/kotoba/client.py +237 -0
  33. kotoba_sdk-0.1.0/src/kotoba/errors.py +55 -0
  34. kotoba_sdk-0.1.0/src/kotoba/models.py +130 -0
  35. kotoba_sdk-0.1.0/src/kotoba/routing.py +77 -0
  36. kotoba_sdk-0.1.0/src/kotoba/s2st.py +151 -0
  37. kotoba_sdk-0.1.0/src/kotoba/tts.py +177 -0
  38. kotoba_sdk-0.1.0/tests/conftest.py +14 -0
  39. kotoba_sdk-0.1.0/tests/test_audio.py +60 -0
  40. kotoba_sdk-0.1.0/tests/test_models.py +42 -0
  41. kotoba_sdk-0.1.0/tests/test_rest_sdk.py +267 -0
  42. kotoba_sdk-0.1.0/tests/test_routing.py +29 -0
  43. kotoba_sdk-0.1.0/tests/test_ws_protocol.py +345 -0
  44. kotoba_sdk-0.1.0/uv.lock +1009 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(env)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ *.swp
11
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kotoba-tech
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.
@@ -0,0 +1,321 @@
1
+ Metadata-Version: 2.4
2
+ Name: kotoba-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Kotoba Speech APIs (REST transcription jobs + WebSocket streaming ASR / TTS / S2ST)
5
+ Project-URL: Homepage, https://kotoba.tech
6
+ Project-URL: Repository, https://github.com/kotoba-tech/kotoba-python
7
+ Author: Kotoba Technologies
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 kotoba-tech
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: asr,kotoba,speech-to-speech,speech-to-text,streaming,tts,websocket
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
39
+ Requires-Python: >=3.10
40
+ Requires-Dist: httpx>=0.27
41
+ Requires-Dist: numpy>=1.26
42
+ Requires-Dist: pydantic>=2.0
43
+ Requires-Dist: requests>=2.31
44
+ Requires-Dist: soundfile>=0.13.1
45
+ Requires-Dist: typing-extensions>=4.10
46
+ Requires-Dist: websockets>=15.0
47
+ Provides-Extra: dev
48
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
49
+ Requires-Dist: pytest>=8.0; extra == 'dev'
50
+ Requires-Dist: responses>=0.25; extra == 'dev'
51
+ Requires-Dist: ruff>=0.5; extra == 'dev'
52
+ Provides-Extra: mic
53
+ Requires-Dist: sounddevice>=0.4.6; extra == 'mic'
54
+ Description-Content-Type: text/markdown
55
+
56
+ # kotoba-sdk
57
+
58
+ Python SDK for Kotoba speech APIs — REST batch transcription and streaming **ASR**, **TTS**, and **speech-to-speech translation** over WebSockets.
59
+
60
+ > Phase-1 alpha. See [`docs/quickstart.md`](docs/quickstart.md).
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install kotoba-sdk
66
+ ```
67
+
68
+ Or from a checkout:
69
+
70
+ ```bash
71
+ git clone https://github.com/kotoba-tech/kotoba-python.git
72
+ cd kotoba-python
73
+ uv venv
74
+ uv pip install -e .
75
+ ```
76
+
77
+ Python ≥ 3.10. Optional `mic` extra (`pip install 'kotoba-sdk[mic]'`) installs `sounddevice` for live-microphone examples.
78
+
79
+ ## Configure endpoints
80
+
81
+ The SDK reads configuration from these env vars only — set the ones for the routes you actually need:
82
+
83
+ | Variable | Purpose |
84
+ |---|---|
85
+ | `KOTOBA_API_KEY` | Bearer token sent as `Authorization: Bearer …` (REST + WS) |
86
+ | `KOTOBA_ASR_REST_URL` | REST API base URL including version prefix, e.g. `https://.../v1` |
87
+ | `KOTOBA_ASR_URL` | WebSocket URL for live ASR, e.g. `wss://.../asr` |
88
+ | `KOTOBA_TTS_JA_URL` | WebSocket URL for Japanese TTS, e.g. `wss://.../tts` |
89
+ | `KOTOBA_S2ST_EN_JA_URL` | WebSocket URL for English-to-Japanese speech translation |
90
+
91
+ You can also register routes from code:
92
+
93
+ ```python
94
+ import kotoba
95
+ kotoba.register_endpoint("tts", None, "ko", "wss://.../tts")
96
+ ```
97
+
98
+ URLs passed explicitly via `url=...` on a call take precedence over the registry.
99
+
100
+ ## Quickstart
101
+
102
+ ```python
103
+ import kotoba
104
+
105
+ client = kotoba.KotobaClient() # reads KOTOBA_API_KEY + KOTOBA_*_URL from env
106
+
107
+ # 1) Speech recognition (REST batch — default for files)
108
+ result = client.asr.transcribe(
109
+ "examples/audio/ja/example.mp3", language="ja"
110
+ )
111
+ print(result.text)
112
+
113
+ # 2) Text-to-Speech (Japanese, default speaker)
114
+ audio = client.tts.synthesize("こんにちは、世界。", language="ja")
115
+ audio.to_wav("hello.wav")
116
+
117
+ # 3) Speech-to-Speech translation (English -> Japanese)
118
+ translated = client.s2st.translate(
119
+ "examples/audio/en/example.mp3", src="en", tgt="ja"
120
+ )
121
+ translated.to_wav("translated.wav")
122
+ print(translated.transcript_source)
123
+ ```
124
+
125
+ `KotobaClient()` reads its credentials and URLs from env vars. Pass them explicitly if you'd rather not rely on the environment:
126
+
127
+ ```python
128
+ client = kotoba.KotobaClient(
129
+ api_key="sk_...",
130
+ url="https://.../v1", # REST base
131
+ asr_ws_url="wss://.../asr",
132
+ tts_ja_ws_url="wss://.../tts",
133
+ s2st_en_ja_ws_url="wss://.../sts",
134
+ )
135
+ ```
136
+
137
+ ## Streaming (the live surface)
138
+
139
+ ASR, TTS, and S2ST are all streaming-first. Audio chunks and partial transcripts surface the moment the server emits them, so you can play / display incrementally instead of waiting for the full response.
140
+
141
+ ### Streaming output
142
+
143
+ ASR streams transcript deltas as audio arrives; TTS streams audio chunks
144
+ as the server produces them from a single text prompt. ASR accepts a
145
+ generator of PCM16 chunks on the input side (feed + drain run
146
+ concurrently); TTS sends the full text in one frame and streams audio
147
+ back:
148
+
149
+ ```python
150
+ # ASR: pcm16 bytes in -> transcript deltas out
151
+ for delta in client.asr.transcribe_stream(mic_chunks(), language="ja"):
152
+ print(delta, end="", flush=True)
153
+
154
+ # TTS: full text in -> pcm audio chunks streamed out
155
+ for pcm in client.tts.synthesize_stream("こんにちは、世界。", language="ja"):
156
+ speaker.write(pcm)
157
+ ```
158
+
159
+ ### Async (recommended for production)
160
+
161
+ ```python
162
+ import asyncio, kotoba
163
+
164
+ async def main():
165
+ client = kotoba.AsyncKotobaClient()
166
+
167
+ async with client.tts.stream(language="ja") as session:
168
+ await session.synthesize("こんにちは。本日はよろしくお願いします。")
169
+
170
+ async for event in session:
171
+ if event.type == "audio_chunk":
172
+ await play(event.audio)
173
+ elif event.type == "done":
174
+ break
175
+
176
+ asyncio.run(main())
177
+ ```
178
+
179
+ ### Sync (notebooks, scripts)
180
+
181
+ ```python
182
+ import kotoba
183
+
184
+ client = kotoba.KotobaClient()
185
+ with client.s2st.stream(src="en", tgt="ja") as session:
186
+ for chunk in pcm16_chunks_from_mic():
187
+ session.send_audio(chunk)
188
+ session.commit()
189
+ for event in session:
190
+ if event.type == "partial_transcript":
191
+ print(event.text, end="", flush=True)
192
+ elif event.type == "audio_chunk":
193
+ speaker.write(event.audio)
194
+ elif event.type == "done":
195
+ break
196
+ ```
197
+
198
+ The sync wrapper runs an `asyncio` loop on a background daemon thread, so the underlying transport is identical — only the call style differs.
199
+
200
+ ## What's in the box
201
+
202
+ | Module | What |
203
+ |---|---|
204
+ | `kotoba.KotobaClient` / `AsyncKotobaClient` | Top-level entry point |
205
+ | `client.asr.transcribe(path, ...)` | **REST** batch transcription with optional `with_timestamps=True` |
206
+ | `client.asr.stream(...)` / `transcribe_stream(iter)` | Streaming ASR (Japanese, English) over WebSocket |
207
+ | `client.tts.stream(...)` / `synthesize(...)` / `synthesize_stream(...)` | Streaming TTS (Japanese) |
208
+ | `client.s2st.stream(...)` / `translate(...)` | Streaming speech-to-speech translation |
209
+ | `kotoba.register_endpoint(...)` | Add `(modality, src, tgt) -> URL` routes |
210
+ | `kotoba.audio.*` | PCM16 / float32 WAV helpers |
211
+
212
+ ## Examples
213
+
214
+ Each example under `examples/` is runnable with `uv run examples/<file>.py` and uses bundled audio under `examples/audio/` by default.
215
+
216
+ | File | What it shows | Required env |
217
+ |---|---|---|
218
+ | `asr_rest_sync.py` | REST batch transcription with `with_timestamps=True`, sync | `KOTOBA_API_KEY`, `KOTOBA_ASR_REST_URL` |
219
+ | `asr_rest_async.py` | Same, async with `AsyncKotobaClient` context manager | `KOTOBA_API_KEY`, `KOTOBA_ASR_REST_URL` |
220
+ | `asr_stream_async.py` | Live ASR via `transcribe_stream(generator)` with first-token-latency measurement | `KOTOBA_API_KEY`, `KOTOBA_ASR_URL` |
221
+ | `tts_synthesize_sync.py` | One-shot TTS with explicit `speaker_id` | `KOTOBA_API_KEY`, `KOTOBA_TTS_JA_URL` |
222
+ | `tts_stream_async.py` | One-shot text in → streamed audio chunks with first-audio-latency timing | `KOTOBA_API_KEY`, `KOTOBA_TTS_JA_URL` |
223
+ | `s2st_stream_async.py` | File in → live transcript + translated WAV out | `KOTOBA_API_KEY`, `KOTOBA_S2ST_EN_JA_URL` |
224
+ | `s2st_mic_async.py` | **Live microphone** in → translated WAV out (Ctrl-C to stop). Requires `pip install 'kotoba-sdk[mic]'` and PortAudio. | `KOTOBA_API_KEY`, `KOTOBA_S2ST_EN_JA_URL` |
225
+
226
+ REST is shown in both sync + async because the context-manager pattern matters for resource cleanup. Streaming examples are async-by-default — wrap with `kotoba.KotobaClient()` for sync (the snippets above show the conversion).
227
+
228
+ ## Public API
229
+
230
+ ### `kotoba.KotobaClient` / `kotoba.AsyncKotobaClient`
231
+
232
+ ```python
233
+ KotobaClient(
234
+ *,
235
+ api_key: str | None = None, # KOTOBA_API_KEY
236
+ url: str | None = None, # KOTOBA_ASR_REST_URL (REST)
237
+ asr_ws_url: str | None = None, # KOTOBA_ASR_URL (WS ASR)
238
+ tts_ja_ws_url: str | None = None, # KOTOBA_TTS_JA_URL (WS TTS)
239
+ s2st_en_ja_ws_url: str | None = None, # KOTOBA_S2ST_EN_JA_URL
240
+ timeout: float = 30.0, # per-request HTTP timeout (s)
241
+ max_retries: int = 3, # for 429/5xx and network errors
242
+ )
243
+ ```
244
+
245
+ Exposes:
246
+
247
+ - `.asr` — `ASRClient` / `AsyncASRClient` (REST + WS)
248
+ - `.tts` — `TTSClient` / `AsyncTTSClient` (WS)
249
+ - `.s2st` — `S2STClient` / `AsyncS2STClient` (WS)
250
+
251
+ The async variant supports `async with …` and exposes `await client.close()`.
252
+
253
+ ### `client.asr.transcribe(...)` — REST batch helper
254
+
255
+ ```python
256
+ transcribe(
257
+ audio_file_path: str | Path,
258
+ *,
259
+ language: str = "ja",
260
+ with_timestamps: bool = False, # ask server for per-segment timestamps
261
+ poll_interval: float = 1.0, # initial GET polling interval (s)
262
+ poll_backoff: float = 1.5, # multiplied each poll
263
+ max_poll_interval: float = 10.0,
264
+ timeout: float = 1200.0, # overall deadline for job completion
265
+ ) -> TranscriptResult
266
+ ```
267
+
268
+ POSTs the file, polls `GET /transcription_jobs/{id}` with exponential backoff, returns the final transcript. Raises `TranscriptionError` on server-reported failure, `TimeoutError` if the deadline elapses.
269
+
270
+ When `with_timestamps=True`, `TranscriptResult.segments` is populated with `[Segment(text, start, end), …]`.
271
+
272
+ ### Low-level REST helpers
273
+
274
+ ```python
275
+ client.asr.submit_job(path, language="ja") -> JobIDResponse # POST
276
+ client.asr.get_job(job_id) -> JobStatus # GET, 202→processing
277
+ ```
278
+
279
+ `JobStatus.state` is one of `JobState.processing | done | error`. For `done`, read `.transcription`; for `error`, read `.error_message`.
280
+
281
+ ### WebSocket entry points
282
+
283
+ ```python
284
+ client.asr.stream(language="ja", url=...) -> ASRSession
285
+ client.asr.transcribe_stream(audio_iter, ...) -> Iterator[str]
286
+
287
+ client.tts.stream(language="ja", speaker_id=..., url=...) -> TTSSession
288
+ client.tts.synthesize_stream(text, ...) -> Iterator[bytes]
289
+ client.tts.synthesize(text, ...) -> AudioResult
290
+
291
+ client.s2st.stream(src="en", tgt="ja", url=...) -> S2STSession
292
+ client.s2st.translate(path, src="en", tgt="ja") -> S2STResult
293
+ ```
294
+
295
+ URLs resolve from the per-route env vars (`KOTOBA_ASR_URL`, `KOTOBA_TTS_JA_URL`, `KOTOBA_S2ST_EN_JA_URL`) unless passed explicitly with `url=`.
296
+
297
+ ### Exceptions
298
+
299
+ All inherit from `kotoba.KotobaError`:
300
+
301
+ | Exception | When |
302
+ |---|---|
303
+ | `AuthError` | HTTP 401/403, WS auth rejection |
304
+ | `ProtocolError` | Other 4xx, or a server `error` frame violating the contract |
305
+ | `APIError` | Transport or 5xx that exhausted retries |
306
+ | `TimeoutError` | HTTP timeout, WS handshake timeout, or `transcribe()` polling deadline exceeded |
307
+ | `JobNotFoundError` | GET returned 404 |
308
+ | `TranscriptionError` | Job completed in `error` state |
309
+ | `UnsupportedRouteError` | No WS URL registered for the requested `(modality, src, tgt)` |
310
+
311
+ ### Retry behavior (REST)
312
+
313
+ Both sync and async clients retry on network errors, 429, and 5xx with exponential backoff. `Retry-After` headers on 429 are honored (async client). 4xx other than 429 raise immediately.
314
+
315
+ ## Development
316
+
317
+ ```bash
318
+ uv venv
319
+ uv pip install -e ".[dev]"
320
+ uv run pytest
321
+ ```
@@ -0,0 +1,266 @@
1
+ # kotoba-sdk
2
+
3
+ Python SDK for Kotoba speech APIs — REST batch transcription and streaming **ASR**, **TTS**, and **speech-to-speech translation** over WebSockets.
4
+
5
+ > Phase-1 alpha. See [`docs/quickstart.md`](docs/quickstart.md).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install kotoba-sdk
11
+ ```
12
+
13
+ Or from a checkout:
14
+
15
+ ```bash
16
+ git clone https://github.com/kotoba-tech/kotoba-python.git
17
+ cd kotoba-python
18
+ uv venv
19
+ uv pip install -e .
20
+ ```
21
+
22
+ Python ≥ 3.10. Optional `mic` extra (`pip install 'kotoba-sdk[mic]'`) installs `sounddevice` for live-microphone examples.
23
+
24
+ ## Configure endpoints
25
+
26
+ The SDK reads configuration from these env vars only — set the ones for the routes you actually need:
27
+
28
+ | Variable | Purpose |
29
+ |---|---|
30
+ | `KOTOBA_API_KEY` | Bearer token sent as `Authorization: Bearer …` (REST + WS) |
31
+ | `KOTOBA_ASR_REST_URL` | REST API base URL including version prefix, e.g. `https://.../v1` |
32
+ | `KOTOBA_ASR_URL` | WebSocket URL for live ASR, e.g. `wss://.../asr` |
33
+ | `KOTOBA_TTS_JA_URL` | WebSocket URL for Japanese TTS, e.g. `wss://.../tts` |
34
+ | `KOTOBA_S2ST_EN_JA_URL` | WebSocket URL for English-to-Japanese speech translation |
35
+
36
+ You can also register routes from code:
37
+
38
+ ```python
39
+ import kotoba
40
+ kotoba.register_endpoint("tts", None, "ko", "wss://.../tts")
41
+ ```
42
+
43
+ URLs passed explicitly via `url=...` on a call take precedence over the registry.
44
+
45
+ ## Quickstart
46
+
47
+ ```python
48
+ import kotoba
49
+
50
+ client = kotoba.KotobaClient() # reads KOTOBA_API_KEY + KOTOBA_*_URL from env
51
+
52
+ # 1) Speech recognition (REST batch — default for files)
53
+ result = client.asr.transcribe(
54
+ "examples/audio/ja/example.mp3", language="ja"
55
+ )
56
+ print(result.text)
57
+
58
+ # 2) Text-to-Speech (Japanese, default speaker)
59
+ audio = client.tts.synthesize("こんにちは、世界。", language="ja")
60
+ audio.to_wav("hello.wav")
61
+
62
+ # 3) Speech-to-Speech translation (English -> Japanese)
63
+ translated = client.s2st.translate(
64
+ "examples/audio/en/example.mp3", src="en", tgt="ja"
65
+ )
66
+ translated.to_wav("translated.wav")
67
+ print(translated.transcript_source)
68
+ ```
69
+
70
+ `KotobaClient()` reads its credentials and URLs from env vars. Pass them explicitly if you'd rather not rely on the environment:
71
+
72
+ ```python
73
+ client = kotoba.KotobaClient(
74
+ api_key="sk_...",
75
+ url="https://.../v1", # REST base
76
+ asr_ws_url="wss://.../asr",
77
+ tts_ja_ws_url="wss://.../tts",
78
+ s2st_en_ja_ws_url="wss://.../sts",
79
+ )
80
+ ```
81
+
82
+ ## Streaming (the live surface)
83
+
84
+ ASR, TTS, and S2ST are all streaming-first. Audio chunks and partial transcripts surface the moment the server emits them, so you can play / display incrementally instead of waiting for the full response.
85
+
86
+ ### Streaming output
87
+
88
+ ASR streams transcript deltas as audio arrives; TTS streams audio chunks
89
+ as the server produces them from a single text prompt. ASR accepts a
90
+ generator of PCM16 chunks on the input side (feed + drain run
91
+ concurrently); TTS sends the full text in one frame and streams audio
92
+ back:
93
+
94
+ ```python
95
+ # ASR: pcm16 bytes in -> transcript deltas out
96
+ for delta in client.asr.transcribe_stream(mic_chunks(), language="ja"):
97
+ print(delta, end="", flush=True)
98
+
99
+ # TTS: full text in -> pcm audio chunks streamed out
100
+ for pcm in client.tts.synthesize_stream("こんにちは、世界。", language="ja"):
101
+ speaker.write(pcm)
102
+ ```
103
+
104
+ ### Async (recommended for production)
105
+
106
+ ```python
107
+ import asyncio, kotoba
108
+
109
+ async def main():
110
+ client = kotoba.AsyncKotobaClient()
111
+
112
+ async with client.tts.stream(language="ja") as session:
113
+ await session.synthesize("こんにちは。本日はよろしくお願いします。")
114
+
115
+ async for event in session:
116
+ if event.type == "audio_chunk":
117
+ await play(event.audio)
118
+ elif event.type == "done":
119
+ break
120
+
121
+ asyncio.run(main())
122
+ ```
123
+
124
+ ### Sync (notebooks, scripts)
125
+
126
+ ```python
127
+ import kotoba
128
+
129
+ client = kotoba.KotobaClient()
130
+ with client.s2st.stream(src="en", tgt="ja") as session:
131
+ for chunk in pcm16_chunks_from_mic():
132
+ session.send_audio(chunk)
133
+ session.commit()
134
+ for event in session:
135
+ if event.type == "partial_transcript":
136
+ print(event.text, end="", flush=True)
137
+ elif event.type == "audio_chunk":
138
+ speaker.write(event.audio)
139
+ elif event.type == "done":
140
+ break
141
+ ```
142
+
143
+ The sync wrapper runs an `asyncio` loop on a background daemon thread, so the underlying transport is identical — only the call style differs.
144
+
145
+ ## What's in the box
146
+
147
+ | Module | What |
148
+ |---|---|
149
+ | `kotoba.KotobaClient` / `AsyncKotobaClient` | Top-level entry point |
150
+ | `client.asr.transcribe(path, ...)` | **REST** batch transcription with optional `with_timestamps=True` |
151
+ | `client.asr.stream(...)` / `transcribe_stream(iter)` | Streaming ASR (Japanese, English) over WebSocket |
152
+ | `client.tts.stream(...)` / `synthesize(...)` / `synthesize_stream(...)` | Streaming TTS (Japanese) |
153
+ | `client.s2st.stream(...)` / `translate(...)` | Streaming speech-to-speech translation |
154
+ | `kotoba.register_endpoint(...)` | Add `(modality, src, tgt) -> URL` routes |
155
+ | `kotoba.audio.*` | PCM16 / float32 WAV helpers |
156
+
157
+ ## Examples
158
+
159
+ Each example under `examples/` is runnable with `uv run examples/<file>.py` and uses bundled audio under `examples/audio/` by default.
160
+
161
+ | File | What it shows | Required env |
162
+ |---|---|---|
163
+ | `asr_rest_sync.py` | REST batch transcription with `with_timestamps=True`, sync | `KOTOBA_API_KEY`, `KOTOBA_ASR_REST_URL` |
164
+ | `asr_rest_async.py` | Same, async with `AsyncKotobaClient` context manager | `KOTOBA_API_KEY`, `KOTOBA_ASR_REST_URL` |
165
+ | `asr_stream_async.py` | Live ASR via `transcribe_stream(generator)` with first-token-latency measurement | `KOTOBA_API_KEY`, `KOTOBA_ASR_URL` |
166
+ | `tts_synthesize_sync.py` | One-shot TTS with explicit `speaker_id` | `KOTOBA_API_KEY`, `KOTOBA_TTS_JA_URL` |
167
+ | `tts_stream_async.py` | One-shot text in → streamed audio chunks with first-audio-latency timing | `KOTOBA_API_KEY`, `KOTOBA_TTS_JA_URL` |
168
+ | `s2st_stream_async.py` | File in → live transcript + translated WAV out | `KOTOBA_API_KEY`, `KOTOBA_S2ST_EN_JA_URL` |
169
+ | `s2st_mic_async.py` | **Live microphone** in → translated WAV out (Ctrl-C to stop). Requires `pip install 'kotoba-sdk[mic]'` and PortAudio. | `KOTOBA_API_KEY`, `KOTOBA_S2ST_EN_JA_URL` |
170
+
171
+ REST is shown in both sync + async because the context-manager pattern matters for resource cleanup. Streaming examples are async-by-default — wrap with `kotoba.KotobaClient()` for sync (the snippets above show the conversion).
172
+
173
+ ## Public API
174
+
175
+ ### `kotoba.KotobaClient` / `kotoba.AsyncKotobaClient`
176
+
177
+ ```python
178
+ KotobaClient(
179
+ *,
180
+ api_key: str | None = None, # KOTOBA_API_KEY
181
+ url: str | None = None, # KOTOBA_ASR_REST_URL (REST)
182
+ asr_ws_url: str | None = None, # KOTOBA_ASR_URL (WS ASR)
183
+ tts_ja_ws_url: str | None = None, # KOTOBA_TTS_JA_URL (WS TTS)
184
+ s2st_en_ja_ws_url: str | None = None, # KOTOBA_S2ST_EN_JA_URL
185
+ timeout: float = 30.0, # per-request HTTP timeout (s)
186
+ max_retries: int = 3, # for 429/5xx and network errors
187
+ )
188
+ ```
189
+
190
+ Exposes:
191
+
192
+ - `.asr` — `ASRClient` / `AsyncASRClient` (REST + WS)
193
+ - `.tts` — `TTSClient` / `AsyncTTSClient` (WS)
194
+ - `.s2st` — `S2STClient` / `AsyncS2STClient` (WS)
195
+
196
+ The async variant supports `async with …` and exposes `await client.close()`.
197
+
198
+ ### `client.asr.transcribe(...)` — REST batch helper
199
+
200
+ ```python
201
+ transcribe(
202
+ audio_file_path: str | Path,
203
+ *,
204
+ language: str = "ja",
205
+ with_timestamps: bool = False, # ask server for per-segment timestamps
206
+ poll_interval: float = 1.0, # initial GET polling interval (s)
207
+ poll_backoff: float = 1.5, # multiplied each poll
208
+ max_poll_interval: float = 10.0,
209
+ timeout: float = 1200.0, # overall deadline for job completion
210
+ ) -> TranscriptResult
211
+ ```
212
+
213
+ POSTs the file, polls `GET /transcription_jobs/{id}` with exponential backoff, returns the final transcript. Raises `TranscriptionError` on server-reported failure, `TimeoutError` if the deadline elapses.
214
+
215
+ When `with_timestamps=True`, `TranscriptResult.segments` is populated with `[Segment(text, start, end), …]`.
216
+
217
+ ### Low-level REST helpers
218
+
219
+ ```python
220
+ client.asr.submit_job(path, language="ja") -> JobIDResponse # POST
221
+ client.asr.get_job(job_id) -> JobStatus # GET, 202→processing
222
+ ```
223
+
224
+ `JobStatus.state` is one of `JobState.processing | done | error`. For `done`, read `.transcription`; for `error`, read `.error_message`.
225
+
226
+ ### WebSocket entry points
227
+
228
+ ```python
229
+ client.asr.stream(language="ja", url=...) -> ASRSession
230
+ client.asr.transcribe_stream(audio_iter, ...) -> Iterator[str]
231
+
232
+ client.tts.stream(language="ja", speaker_id=..., url=...) -> TTSSession
233
+ client.tts.synthesize_stream(text, ...) -> Iterator[bytes]
234
+ client.tts.synthesize(text, ...) -> AudioResult
235
+
236
+ client.s2st.stream(src="en", tgt="ja", url=...) -> S2STSession
237
+ client.s2st.translate(path, src="en", tgt="ja") -> S2STResult
238
+ ```
239
+
240
+ URLs resolve from the per-route env vars (`KOTOBA_ASR_URL`, `KOTOBA_TTS_JA_URL`, `KOTOBA_S2ST_EN_JA_URL`) unless passed explicitly with `url=`.
241
+
242
+ ### Exceptions
243
+
244
+ All inherit from `kotoba.KotobaError`:
245
+
246
+ | Exception | When |
247
+ |---|---|
248
+ | `AuthError` | HTTP 401/403, WS auth rejection |
249
+ | `ProtocolError` | Other 4xx, or a server `error` frame violating the contract |
250
+ | `APIError` | Transport or 5xx that exhausted retries |
251
+ | `TimeoutError` | HTTP timeout, WS handshake timeout, or `transcribe()` polling deadline exceeded |
252
+ | `JobNotFoundError` | GET returned 404 |
253
+ | `TranscriptionError` | Job completed in `error` state |
254
+ | `UnsupportedRouteError` | No WS URL registered for the requested `(modality, src, tgt)` |
255
+
256
+ ### Retry behavior (REST)
257
+
258
+ Both sync and async clients retry on network errors, 429, and 5xx with exponential backoff. `Retry-After` headers on 429 are honored (async client). 4xx other than 429 raise immediately.
259
+
260
+ ## Development
261
+
262
+ ```bash
263
+ uv venv
264
+ uv pip install -e ".[dev]"
265
+ uv run pytest
266
+ ```