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 +37 -0
- buildsol/_async_resources.py +254 -0
- buildsol/_resources.py +224 -0
- buildsol/_transport.py +286 -0
- buildsol/audio.py +72 -0
- buildsol/client.py +78 -0
- buildsol/errors.py +69 -0
- buildsol/live.py +171 -0
- buildsol/py.typed +0 -0
- buildsol/types.py +106 -0
- buildsol-0.1.1.dist-info/METADATA +134 -0
- buildsol-0.1.1.dist-info/RECORD +14 -0
- buildsol-0.1.1.dist-info/WHEEL +4 -0
- buildsol-0.1.1.dist-info/licenses/LICENSE +21 -0
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
|