buildsol 0.1.1__py3-none-any.whl

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.
buildsol/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """Official Sol Python SDK. Keep your API key on your server."""
2
+
3
+ from ._transport import VERSION as __version__
4
+ from .audio import AsyncAudioStream, Audio, AudioStream
5
+ from .client import AsyncSol, Sol
6
+ from .errors import (
7
+ APIError,
8
+ AuthenticationError,
9
+ ConnectionError,
10
+ NotFoundError,
11
+ PermissionDeniedError,
12
+ ProtocolError,
13
+ RateLimitError,
14
+ SolError,
15
+ TimeoutError,
16
+ )
17
+ from .live import AsyncLiveConnection, LiveConnection
18
+
19
+ __all__ = [
20
+ "Sol",
21
+ "AsyncSol",
22
+ "Audio",
23
+ "AudioStream",
24
+ "AsyncAudioStream",
25
+ "SolError",
26
+ "APIError",
27
+ "AuthenticationError",
28
+ "PermissionDeniedError",
29
+ "NotFoundError",
30
+ "RateLimitError",
31
+ "ConnectionError",
32
+ "TimeoutError",
33
+ "ProtocolError",
34
+ "LiveConnection",
35
+ "AsyncLiveConnection",
36
+ "__version__",
37
+ ]
@@ -0,0 +1,254 @@
1
+ from contextlib import asynccontextmanager
2
+ from typing import Any
3
+ from urllib.parse import quote
4
+
5
+ import httpx
6
+ from typing_extensions import Unpack
7
+
8
+ from ._transport import Transport, event, form, transport_error, upload
9
+ from .audio import AsyncAudioStream, Audio
10
+ from .types import (
11
+ Balance,
12
+ LiveSession,
13
+ SpeechParams,
14
+ Transcription,
15
+ TranscriptionParams,
16
+ Translation,
17
+ Upload,
18
+ VoiceList,
19
+ VoiceParams,
20
+ VoiceResult,
21
+ )
22
+
23
+
24
+ class Resource:
25
+ def __init__(self, transport: Transport):
26
+ self._t = transport
27
+
28
+
29
+ class Speech(Resource):
30
+ @asynccontextmanager
31
+ async def stream(
32
+ self,
33
+ *,
34
+ reference_file: Upload | None = None,
35
+ timeout: float | None = None,
36
+ **params: Unpack[SpeechParams],
37
+ ):
38
+ kwargs = (
39
+ {"json": params}
40
+ if reference_file is None
41
+ else {
42
+ "data": form(params),
43
+ "files": {"reference_audio": upload(reference_file)},
44
+ }
45
+ )
46
+ response = await self._t.arequest(
47
+ "POST", "/v1/text-to-speech", timeout=timeout, **kwargs
48
+ )
49
+ try:
50
+ yield AsyncAudioStream(response)
51
+ finally:
52
+ await response.aclose()
53
+
54
+ async def create(
55
+ self,
56
+ *,
57
+ reference_file: Upload | None = None,
58
+ timeout: float | None = None,
59
+ **params: Unpack[SpeechParams],
60
+ ) -> Audio:
61
+ async with self.stream(
62
+ reference_file=reference_file, timeout=timeout, **params
63
+ ) as stream:
64
+ content = b"".join([chunk async for chunk in stream])
65
+ return Audio(content, stream.headers)
66
+
67
+
68
+ class Voices(Resource):
69
+ async def list(
70
+ self,
71
+ *,
72
+ country: str | None = None,
73
+ gender: str | None = None,
74
+ age: str | None = None,
75
+ category: str | None = None,
76
+ timeout: float | None = None,
77
+ ) -> VoiceList:
78
+ return await self._t.ajson(
79
+ "GET",
80
+ "/v1/voices",
81
+ params=form(
82
+ {"country": country, "gender": gender, "age": age, "category": category}
83
+ ),
84
+ timeout=timeout,
85
+ )
86
+
87
+ async def get(self, voice_id: str, *, timeout: float | None = None) -> VoiceResult:
88
+ return await self._t.ajson(
89
+ "GET", "/v1/voices/" + quote(voice_id, safe=""), timeout=timeout
90
+ )
91
+
92
+ async def create(
93
+ self,
94
+ *,
95
+ name: str,
96
+ audio: Upload,
97
+ consent: bool,
98
+ timeout: float | None = None,
99
+ **params: Unpack[VoiceParams],
100
+ ) -> VoiceResult:
101
+ return await self._t.ajson(
102
+ "POST",
103
+ "/v1/voices",
104
+ data=form({"name": name, "consent": consent, **params}),
105
+ files={"audio": upload(audio)},
106
+ timeout=timeout,
107
+ )
108
+
109
+ async def delete(
110
+ self, voice_id: str, *, timeout: float | None = None
111
+ ) -> dict[str, bool]:
112
+ return await self._t.ajson(
113
+ "DELETE", "/v1/voices/" + quote(voice_id, safe=""), timeout=timeout
114
+ )
115
+
116
+
117
+ class Controls(Resource):
118
+ async def get(self, *, timeout: float | None = None) -> dict[str, Any]:
119
+ return await self._t.ajson("GET", "/v1/tts/controls", timeout=timeout)
120
+
121
+
122
+ class Dialects(Resource):
123
+ async def list(self, *, timeout: float | None = None) -> dict[str, Any]:
124
+ return await self._t.ajson("GET", "/v1/dialects", timeout=timeout)
125
+
126
+
127
+ class BalanceResource(Resource):
128
+ async def get(self, *, timeout: float | None = None) -> Balance:
129
+ return await self._t.ajson("GET", "/v1/balance", timeout=timeout)
130
+
131
+
132
+ class Transcriptions(Resource):
133
+ async def create(
134
+ self,
135
+ *,
136
+ file: Upload,
137
+ idempotency_key: str | None = None,
138
+ timeout: float | None = None,
139
+ **params: Unpack[TranscriptionParams],
140
+ ) -> Transcription:
141
+ return await self._t.ajson(
142
+ "POST",
143
+ "/v1/audio/transcriptions",
144
+ data=form(params),
145
+ files={"file": upload(file)},
146
+ idempotency_key=idempotency_key,
147
+ timeout=timeout,
148
+ )
149
+
150
+ @asynccontextmanager
151
+ async def stream(
152
+ self,
153
+ *,
154
+ file: Upload,
155
+ idempotency_key: str | None = None,
156
+ timeout: float | None = None,
157
+ **params: Unpack[TranscriptionParams],
158
+ ):
159
+ """SSE transport for a processed file; use live.connect for real-time audio."""
160
+ response = await self._t.arequest(
161
+ "POST",
162
+ "/v1/audio/transcriptions",
163
+ data=form({**params, "stream": True}),
164
+ files={"file": upload(file)},
165
+ idempotency_key=idempotency_key,
166
+ timeout=timeout,
167
+ )
168
+ try:
169
+ yield _sse(response)
170
+ finally:
171
+ await response.aclose()
172
+
173
+
174
+ class Translations(Resource):
175
+ async def create(
176
+ self,
177
+ *,
178
+ text: str,
179
+ source_language: str,
180
+ target_language: str,
181
+ model: str = "sol-v1-preview-translate",
182
+ idempotency_key: str | None = None,
183
+ timeout: float | None = None,
184
+ ) -> Translation:
185
+ return await self._t.ajson(
186
+ "POST",
187
+ "/v1/translate",
188
+ json={
189
+ "text": text,
190
+ "source_language": source_language,
191
+ "target_language": target_language,
192
+ "model": model,
193
+ },
194
+ idempotency_key=idempotency_key,
195
+ timeout=timeout,
196
+ )
197
+
198
+
199
+ class Live(Resource):
200
+ async def create_session(
201
+ self,
202
+ *,
203
+ language: str | None = None,
204
+ model: str = "sol-transcribe-v1",
205
+ timeout: float | None = None,
206
+ ) -> LiveSession:
207
+ return await self._t.ajson(
208
+ "POST",
209
+ "/v1/audio/transcription-sessions",
210
+ json=form({"model": model, "language": language}),
211
+ timeout=timeout,
212
+ )
213
+
214
+ @asynccontextmanager
215
+ async def connect(
216
+ self,
217
+ *,
218
+ session: LiveSession | None = None,
219
+ language: str | None = None,
220
+ timeout: float = 30,
221
+ ):
222
+ from .live import AsyncLiveConnection
223
+
224
+ session = session or await self.create_session(
225
+ language=language, timeout=timeout
226
+ )
227
+ connection = AsyncLiveConnection(session, timeout=timeout)
228
+ async with connection:
229
+ yield connection
230
+
231
+
232
+ async def _sse(response):
233
+ data = []
234
+ completed = False
235
+ try:
236
+ async for line in response.aiter_lines():
237
+ if line == "":
238
+ if data:
239
+ value = event("\n".join(data))
240
+ completed |= value.get("type") == "completed"
241
+ yield value
242
+ data = []
243
+ elif line.startswith("data:"):
244
+ data.append(line[5:].removeprefix(" "))
245
+ if data:
246
+ value = event("\n".join(data))
247
+ completed |= value.get("type") == "completed"
248
+ yield value
249
+ if not completed:
250
+ from .errors import ProtocolError
251
+
252
+ raise ProtocolError("Transcription stream ended without a completed event")
253
+ except httpx.TransportError as exc:
254
+ raise transport_error(exc) from exc
buildsol/_resources.py ADDED
@@ -0,0 +1,224 @@
1
+ from contextlib import contextmanager
2
+ from typing import Any
3
+ from urllib.parse import quote
4
+
5
+ from typing_extensions import Unpack
6
+
7
+ from ._transport import Transport, form, sse, upload
8
+ from .audio import Audio, AudioStream
9
+ from .types import (
10
+ Balance,
11
+ LiveSession,
12
+ SpeechParams,
13
+ Transcription,
14
+ TranscriptionParams,
15
+ Translation,
16
+ Upload,
17
+ VoiceList,
18
+ VoiceParams,
19
+ VoiceResult,
20
+ )
21
+
22
+
23
+ class Resource:
24
+ def __init__(self, transport: Transport):
25
+ self._t = transport
26
+
27
+
28
+ class Speech(Resource):
29
+ @contextmanager
30
+ def stream(
31
+ self,
32
+ *,
33
+ reference_file: Upload | None = None,
34
+ timeout: float | None = None,
35
+ **params: Unpack[SpeechParams],
36
+ ):
37
+ kwargs = (
38
+ {"json": params}
39
+ if reference_file is None
40
+ else {
41
+ "data": form(params),
42
+ "files": {"reference_audio": upload(reference_file)},
43
+ }
44
+ )
45
+ response = self._t.request(
46
+ "POST", "/v1/text-to-speech", timeout=timeout, **kwargs
47
+ )
48
+ try:
49
+ yield AudioStream(response)
50
+ finally:
51
+ response.close()
52
+
53
+ def create(
54
+ self,
55
+ *,
56
+ reference_file: Upload | None = None,
57
+ timeout: float | None = None,
58
+ **params: Unpack[SpeechParams],
59
+ ) -> Audio:
60
+ with self.stream(
61
+ reference_file=reference_file, timeout=timeout, **params
62
+ ) as stream:
63
+ content = b"".join(stream)
64
+ return Audio(content, stream.headers)
65
+
66
+
67
+ class Voices(Resource):
68
+ def list(
69
+ self,
70
+ *,
71
+ country: str | None = None,
72
+ gender: str | None = None,
73
+ age: str | None = None,
74
+ category: str | None = None,
75
+ timeout: float | None = None,
76
+ ) -> VoiceList:
77
+ return self._t.json(
78
+ "GET",
79
+ "/v1/voices",
80
+ params=form(
81
+ {"country": country, "gender": gender, "age": age, "category": category}
82
+ ),
83
+ timeout=timeout,
84
+ )
85
+
86
+ def get(self, voice_id: str, *, timeout: float | None = None) -> VoiceResult:
87
+ return self._t.json(
88
+ "GET", "/v1/voices/" + quote(voice_id, safe=""), timeout=timeout
89
+ )
90
+
91
+ def create(
92
+ self,
93
+ *,
94
+ name: str,
95
+ audio: Upload,
96
+ consent: bool,
97
+ timeout: float | None = None,
98
+ **params: Unpack[VoiceParams],
99
+ ) -> VoiceResult:
100
+ return self._t.json(
101
+ "POST",
102
+ "/v1/voices",
103
+ data=form({"name": name, "consent": consent, **params}),
104
+ files={"audio": upload(audio)},
105
+ timeout=timeout,
106
+ )
107
+
108
+ def delete(self, voice_id: str, *, timeout: float | None = None) -> dict[str, bool]:
109
+ return self._t.json(
110
+ "DELETE", "/v1/voices/" + quote(voice_id, safe=""), timeout=timeout
111
+ )
112
+
113
+
114
+ class Controls(Resource):
115
+ def get(self, *, timeout: float | None = None) -> dict[str, Any]:
116
+ return self._t.json("GET", "/v1/tts/controls", timeout=timeout)
117
+
118
+
119
+ class Dialects(Resource):
120
+ def list(self, *, timeout: float | None = None) -> dict[str, Any]:
121
+ return self._t.json("GET", "/v1/dialects", timeout=timeout)
122
+
123
+
124
+ class BalanceResource(Resource):
125
+ def get(self, *, timeout: float | None = None) -> Balance:
126
+ return self._t.json("GET", "/v1/balance", timeout=timeout)
127
+
128
+
129
+ class Transcriptions(Resource):
130
+ def create(
131
+ self,
132
+ *,
133
+ file: Upload,
134
+ idempotency_key: str | None = None,
135
+ timeout: float | None = None,
136
+ **params: Unpack[TranscriptionParams],
137
+ ) -> Transcription:
138
+ return self._t.json(
139
+ "POST",
140
+ "/v1/audio/transcriptions",
141
+ data=form(params),
142
+ files={"file": upload(file)},
143
+ idempotency_key=idempotency_key,
144
+ timeout=timeout,
145
+ )
146
+
147
+ @contextmanager
148
+ def stream(
149
+ self,
150
+ *,
151
+ file: Upload,
152
+ idempotency_key: str | None = None,
153
+ timeout: float | None = None,
154
+ **params: Unpack[TranscriptionParams],
155
+ ):
156
+ """SSE transport for a processed file; use live.connect for real-time audio."""
157
+ response = self._t.request(
158
+ "POST",
159
+ "/v1/audio/transcriptions",
160
+ data=form({**params, "stream": True}),
161
+ files={"file": upload(file)},
162
+ idempotency_key=idempotency_key,
163
+ timeout=timeout,
164
+ )
165
+ try:
166
+ yield sse(response.iter_lines())
167
+ finally:
168
+ response.close()
169
+
170
+
171
+ class Translations(Resource):
172
+ def create(
173
+ self,
174
+ *,
175
+ text: str,
176
+ source_language: str,
177
+ target_language: str,
178
+ model: str = "sol-v1-preview-translate",
179
+ idempotency_key: str | None = None,
180
+ timeout: float | None = None,
181
+ ) -> Translation:
182
+ return self._t.json(
183
+ "POST",
184
+ "/v1/translate",
185
+ json={
186
+ "text": text,
187
+ "source_language": source_language,
188
+ "target_language": target_language,
189
+ "model": model,
190
+ },
191
+ idempotency_key=idempotency_key,
192
+ timeout=timeout,
193
+ )
194
+
195
+
196
+ class Live(Resource):
197
+ def create_session(
198
+ self,
199
+ *,
200
+ language: str | None = None,
201
+ model: str = "sol-transcribe-v1",
202
+ timeout: float | None = None,
203
+ ) -> LiveSession:
204
+ return self._t.json(
205
+ "POST",
206
+ "/v1/audio/transcription-sessions",
207
+ json=form({"model": model, "language": language}),
208
+ timeout=timeout,
209
+ )
210
+
211
+ @contextmanager
212
+ def connect(
213
+ self,
214
+ *,
215
+ session: LiveSession | None = None,
216
+ language: str | None = None,
217
+ timeout: float = 30,
218
+ ):
219
+ from .live import LiveConnection
220
+
221
+ session = session or self.create_session(language=language, timeout=timeout)
222
+ connection = LiveConnection(session, timeout=timeout)
223
+ with connection:
224
+ yield connection