buildsol 0.1.1__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,15 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ venv/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ node_modules/
9
+ dist/
10
+ *.egg-info/
11
+ *.tgz
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+ .DS_Store
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1
4
+
5
+ - Prepare npm and PyPI distribution with standard registry installation instructions.
6
+ - No public API or runtime behavior changes from 0.1.0.
7
+
8
+ ## 0.1.0
9
+
10
+ Initial Python and JavaScript/TypeScript SDK release:
11
+ - Incremental TTS streaming and file downloads, with audio controls preserved.
12
+ - Voice catalogue, consent-required cloning, deletion, controls, dialects and balance.
13
+ - File transcription (JSON/SSE), live WebSocket transcription and translation.
14
+ - Python sync/async clients; Node ESM/CommonJS and TypeScript declarations.
15
+ - Typed API errors, timeouts, cancellation, safe retries and explicit idempotency.
buildsol-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sol
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,134 @@
1
+ Metadata-Version: 2.5
2
+ Name: buildsol
3
+ Version: 0.1.1
4
+ Summary: Official Sol SDK: streaming speech, voices, transcription and translation.
5
+ Project-URL: Homepage, https://www.buildsol.ai
6
+ Project-URL: Documentation, https://www.buildsol.ai/docs#sdks
7
+ Project-URL: Repository, https://github.com/hectordiazgomez/sol-sdks
8
+ Project-URL: Issues, https://github.com/hectordiazgomez/sol-sdks/issues
9
+ Author: Sol
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: httpx<1,>=0.27
17
+ Requires-Dist: typing-extensions>=4.12
18
+ Requires-Dist: websockets<18,>=14
19
+ Provides-Extra: dev
20
+ Requires-Dist: build>=1.2; extra == 'dev'
21
+ Requires-Dist: mypy>=1.14; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Requires-Dist: ruff>=0.9; extra == 'dev'
25
+ Requires-Dist: twine>=6; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Sol Python SDK
29
+
30
+ Official synchronous and asynchronous Python client. Requires Python 3.10+.
31
+
32
+ ## Install
33
+
34
+ ```sh
35
+ python -m pip install buildsol
36
+ ```
37
+
38
+ Install the official `buildsol` package from [PyPI](https://pypi.org/project/buildsol/). For development, clone this repository and run `pip install -e './python[dev]'`.
39
+
40
+ Set `SOL_API_KEY` on your server, then:
41
+
42
+ ```python
43
+ from buildsol import Sol
44
+
45
+ with Sol() as sol:
46
+ voices = sol.voices.list(country="PE")
47
+ voice_id = voices["voices"][0]["id"] # handle an empty catalogue in your app
48
+ with sol.speech.stream(
49
+ text="Hola, tu pedido llega mañana.", voice_id=voice_id, speed=1.0
50
+ ) as audio:
51
+ print(audio.request_id)
52
+ audio.save("speech.mp3")
53
+ ```
54
+
55
+ `AudioStream` is an iterator of unchanged `bytes`. Always use its context manager so an early exit closes the connection. `speech.create()` returns `Audio` with `.content`, `.headers`, `.request_id` and `.save(path)` after downloading the full result. `reference_file=` supports multipart references; the JSON `reference_audio=` field is base64 text. Do not combine references with `voice_id`.
56
+
57
+ ## Async and incremental bytes
58
+
59
+ ```python
60
+ import asyncio
61
+ from buildsol import AsyncSol
62
+
63
+
64
+ async def main():
65
+ async with AsyncSol() as sol:
66
+ async with sol.speech.stream(text="Buenos días.", output_format="pcm") as audio:
67
+ async for chunk in audio:
68
+ print(len(chunk)) # forward to your own compatible playback pipeline
69
+
70
+
71
+ asyncio.run(main())
72
+ ```
73
+
74
+ ## Transcription, translation and voices
75
+
76
+ ```python
77
+ from buildsol import Sol
78
+
79
+ with Sol() as sol:
80
+ transcript = sol.transcriptions.create(
81
+ file="recording.wav",
82
+ language="es",
83
+ diarize=True,
84
+ transcription_style="verbatim",
85
+ idempotency_key="recording-2026-001",
86
+ )
87
+ print(transcript["text"], transcript["billing"])
88
+ translated = sol.translations.create(
89
+ text="Buenos días.",
90
+ source_language="es",
91
+ target_language="pt",
92
+ idempotency_key="translation-2026-001",
93
+ )
94
+ print(translated["translation"])
95
+ print(sol.controls.get(), sol.dialects.list(), sol.balance.get())
96
+
97
+ # Only with the speaker's permission; creates an account voice:
98
+ # voice = sol.voices.create(name="Mi voz", audio="reference.wav", consent=True)
99
+ # sol.voices.get(voice["voice"]["id"])
100
+ # sol.voices.delete(voice["voice"]["id"]) # irreversible
101
+ ```
102
+
103
+ File inputs accept a path, `Path`, bytes (default filename `audio.wav`), an open binary file, `(filename, bytes)` or `(filename, bytes, content_type)`. Uploads are read into memory once, allowing safe idempotent replay of accepted small uploads. The SDK does not close caller-owned file objects.
104
+
105
+ File SSE:
106
+
107
+ ```python
108
+ with Sol() as sol:
109
+ with sol.transcriptions.stream(file="recording.wav", language="es") as events:
110
+ for event in events:
111
+ print(event) # segment, metadata, completed; an error raises APIError
112
+ ```
113
+
114
+ This does not make file transcription real-time. Use a live session for microphone input. See [live_python.py](https://github.com/hectordiazgomez/sol-sdks/blob/main/examples/live_python.py) for concurrent sending, real-time pacing and finalization. `LiveConnection` and `AsyncLiveConnection` can also consume a pre-issued single-use session. Live receive timeout defaults to 30 seconds; close the connection when done. No token is reused or reconnected automatically.
115
+
116
+ ## Configuration and errors
117
+
118
+ ```python
119
+ from buildsol import Sol, APIError, ConnectionError, TimeoutError
120
+
121
+ with Sol(timeout=120.0, max_retries=2) as sol:
122
+ try:
123
+ balance = sol.balance.get(timeout=15.0)
124
+ except APIError as error:
125
+ print(error.status_code, error.code, error.request_id)
126
+ except TimeoutError:
127
+ print("Request timed out")
128
+ except ConnectionError:
129
+ print("Connection interrupted")
130
+ ```
131
+
132
+ Optional `api_key=`, `base_url=` and `http_client=` configure the client. HTTPX's timeout is a per-network-operation timeout (connect/read/write/pool), in seconds. Cancel an async task normally; `async with` releases resources. `AuthenticationError`, `PermissionDeniedError`, `NotFoundError` and `RateLimitError` subclass `APIError`; `.details` and `.headers` retain server information. `ProtocolError` reports malformed server events/JSON. Never log secrets or full audio.
133
+
134
+ Retries are limited to GET or explicit-idempotency transcription/translation requests; **TTS is never automatically retried**. Partial-stream failures preserve the partial file and raise an error; the caller should remove or label incomplete files. See the repository README for full API coverage, limits, token rules and billing distinctions.
@@ -0,0 +1,107 @@
1
+ # Sol Python SDK
2
+
3
+ Official synchronous and asynchronous Python client. Requires Python 3.10+.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ python -m pip install buildsol
9
+ ```
10
+
11
+ Install the official `buildsol` package from [PyPI](https://pypi.org/project/buildsol/). For development, clone this repository and run `pip install -e './python[dev]'`.
12
+
13
+ Set `SOL_API_KEY` on your server, then:
14
+
15
+ ```python
16
+ from buildsol import Sol
17
+
18
+ with Sol() as sol:
19
+ voices = sol.voices.list(country="PE")
20
+ voice_id = voices["voices"][0]["id"] # handle an empty catalogue in your app
21
+ with sol.speech.stream(
22
+ text="Hola, tu pedido llega mañana.", voice_id=voice_id, speed=1.0
23
+ ) as audio:
24
+ print(audio.request_id)
25
+ audio.save("speech.mp3")
26
+ ```
27
+
28
+ `AudioStream` is an iterator of unchanged `bytes`. Always use its context manager so an early exit closes the connection. `speech.create()` returns `Audio` with `.content`, `.headers`, `.request_id` and `.save(path)` after downloading the full result. `reference_file=` supports multipart references; the JSON `reference_audio=` field is base64 text. Do not combine references with `voice_id`.
29
+
30
+ ## Async and incremental bytes
31
+
32
+ ```python
33
+ import asyncio
34
+ from buildsol import AsyncSol
35
+
36
+
37
+ async def main():
38
+ async with AsyncSol() as sol:
39
+ async with sol.speech.stream(text="Buenos días.", output_format="pcm") as audio:
40
+ async for chunk in audio:
41
+ print(len(chunk)) # forward to your own compatible playback pipeline
42
+
43
+
44
+ asyncio.run(main())
45
+ ```
46
+
47
+ ## Transcription, translation and voices
48
+
49
+ ```python
50
+ from buildsol import Sol
51
+
52
+ with Sol() as sol:
53
+ transcript = sol.transcriptions.create(
54
+ file="recording.wav",
55
+ language="es",
56
+ diarize=True,
57
+ transcription_style="verbatim",
58
+ idempotency_key="recording-2026-001",
59
+ )
60
+ print(transcript["text"], transcript["billing"])
61
+ translated = sol.translations.create(
62
+ text="Buenos días.",
63
+ source_language="es",
64
+ target_language="pt",
65
+ idempotency_key="translation-2026-001",
66
+ )
67
+ print(translated["translation"])
68
+ print(sol.controls.get(), sol.dialects.list(), sol.balance.get())
69
+
70
+ # Only with the speaker's permission; creates an account voice:
71
+ # voice = sol.voices.create(name="Mi voz", audio="reference.wav", consent=True)
72
+ # sol.voices.get(voice["voice"]["id"])
73
+ # sol.voices.delete(voice["voice"]["id"]) # irreversible
74
+ ```
75
+
76
+ File inputs accept a path, `Path`, bytes (default filename `audio.wav`), an open binary file, `(filename, bytes)` or `(filename, bytes, content_type)`. Uploads are read into memory once, allowing safe idempotent replay of accepted small uploads. The SDK does not close caller-owned file objects.
77
+
78
+ File SSE:
79
+
80
+ ```python
81
+ with Sol() as sol:
82
+ with sol.transcriptions.stream(file="recording.wav", language="es") as events:
83
+ for event in events:
84
+ print(event) # segment, metadata, completed; an error raises APIError
85
+ ```
86
+
87
+ This does not make file transcription real-time. Use a live session for microphone input. See [live_python.py](https://github.com/hectordiazgomez/sol-sdks/blob/main/examples/live_python.py) for concurrent sending, real-time pacing and finalization. `LiveConnection` and `AsyncLiveConnection` can also consume a pre-issued single-use session. Live receive timeout defaults to 30 seconds; close the connection when done. No token is reused or reconnected automatically.
88
+
89
+ ## Configuration and errors
90
+
91
+ ```python
92
+ from buildsol import Sol, APIError, ConnectionError, TimeoutError
93
+
94
+ with Sol(timeout=120.0, max_retries=2) as sol:
95
+ try:
96
+ balance = sol.balance.get(timeout=15.0)
97
+ except APIError as error:
98
+ print(error.status_code, error.code, error.request_id)
99
+ except TimeoutError:
100
+ print("Request timed out")
101
+ except ConnectionError:
102
+ print("Connection interrupted")
103
+ ```
104
+
105
+ Optional `api_key=`, `base_url=` and `http_client=` configure the client. HTTPX's timeout is a per-network-operation timeout (connect/read/write/pool), in seconds. Cancel an async task normally; `async with` releases resources. `AuthenticationError`, `PermissionDeniedError`, `NotFoundError` and `RateLimitError` subclass `APIError`; `.details` and `.headers` retain server information. `ProtocolError` reports malformed server events/JSON. Never log secrets or full audio.
106
+
107
+ Retries are limited to GET or explicit-idempotency transcription/translation requests; **TTS is never automatically retried**. Partial-stream failures preserve the partial file and raise an error; the caller should remove or label incomplete files. See the repository README for full API coverage, limits, token rules and billing distinctions.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26"]
3
+ build-backend = "hatchling.build"
4
+ [project]
5
+ name = "buildsol"
6
+ version = "0.1.1"
7
+ description = "Official Sol SDK: streaming speech, voices, transcription and translation."
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ authors = [{name = "Sol"}]
12
+ dependencies = ["httpx>=0.27,<1", "websockets>=14,<18", "typing-extensions>=4.12"]
13
+ classifiers = ["Programming Language :: Python :: 3", "Typing :: Typed", "Operating System :: OS Independent"]
14
+ [project.urls]
15
+ Homepage = "https://www.buildsol.ai"
16
+ Documentation = "https://www.buildsol.ai/docs#sdks"
17
+ Repository = "https://github.com/hectordiazgomez/sol-sdks"
18
+ Issues = "https://github.com/hectordiazgomez/sol-sdks/issues"
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/buildsol"]
21
+ [project.optional-dependencies]
22
+ dev = ["pytest>=8", "pytest-asyncio>=0.24", "build>=1.2", "twine>=6", "ruff>=0.9", "mypy>=1.14"]
23
+ [tool.pytest.ini_options]
24
+ asyncio_mode = "auto"
25
+ asyncio_default_fixture_loop_scope = "function"
@@ -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