vocalstack 1.0.0a1__tar.gz → 1.0.0a2__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.
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/PKG-INFO +7 -2
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/README.md +6 -1
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/SKILL.md +45 -2
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/pyproject.toml +1 -4
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/skills/vocalstack/SKILL.md +45 -2
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/src/vocalstack/__init__.py +1 -1
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/src/vocalstack/_http.py +20 -13
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/src/vocalstack/_models.py +522 -153
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/src/vocalstack/client.py +4 -0
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/src/vocalstack/realtime.py +116 -24
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/src/vocalstack/resources.py +383 -24
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/.gitignore +0 -0
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/LICENSE +0 -0
- {vocalstack-1.0.0a1 → vocalstack-1.0.0a2}/src/vocalstack/errors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vocalstack
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a2
|
|
4
4
|
Summary: VocalStack speech API SDK (asyncio): one client grouped by resource, typed models from the API contract, realtime WebSocket streaming.
|
|
5
5
|
Project-URL: Homepage, https://vocalstack.com
|
|
6
6
|
License-Expression: MIT
|
|
@@ -18,6 +18,8 @@ The VocalStack speech API for Python (3.10 or newer, asyncio): one client,
|
|
|
18
18
|
are pydantic models generated from the API's one OpenAPI document
|
|
19
19
|
(`/v1/openapi.json`), with snake_case attributes.
|
|
20
20
|
|
|
21
|
+
Read more about the [VocalStack Python SDK](https://vocalstack.com/#developers) and our other SDKs on the [VocalStack website](https://vocalstack.com/).
|
|
22
|
+
|
|
21
23
|
```python
|
|
22
24
|
import asyncio, os
|
|
23
25
|
from vocalstack import VocalStack
|
|
@@ -45,7 +47,9 @@ pip install vocalstack
|
|
|
45
47
|
|
|
46
48
|
- [Quickstart](https://github.com/vocalstack/python-sdk/blob/main/docs/quickstart.md): An API key, the SDK, and your first transcript in a dozen lines.
|
|
47
49
|
- [Transcribe a file](https://github.com/vocalstack/python-sdk/blob/main/docs/transcribe-a-file.md): Upload a file, wait for its job, read the transcript and export it. Asynchronous; nothing to keep open.
|
|
48
|
-
- [Transcribe a link](https://github.com/vocalstack/python-sdk/blob/main/docs/transcribe-a-link.md): The server fetches a public URL or a
|
|
50
|
+
- [Transcribe a link](https://github.com/vocalstack/python-sdk/blob/main/docs/transcribe-a-link.md): The server fetches a public URL, a YouTube video or a recording on a video or audio site, then transcribes it like a file. Asynchronous, with a fetch phase first.
|
|
51
|
+
- [Transcribe a batch of files](https://github.com/vocalstack/python-sdk/blob/main/docs/transcribe-a-batch.md): Send many files or links as one batch, start it once, and read every transcript when it is done. Cheaper than one transcription per file.
|
|
52
|
+
- [Search your transcriptions](https://github.com/vocalstack/python-sdk/blob/main/docs/search-transcriptions.md): Find transcriptions by words in their name or link, site, language, kind, tag and date, keep the shared ones, and play each with its share code.
|
|
49
53
|
- [Stream a microphone](https://github.com/vocalstack/python-sdk/blob/main/docs/stream-a-microphone.md): Live audio in, transcript events out, over one WebSocket. Drafts replace the live tail; commits append.
|
|
50
54
|
- [Stream a file at full speed](https://github.com/vocalstack/python-sdk/blob/main/docs/stream-a-file.md): Send a file's bytes through a realtime session and read the transcript as fast as the server makes it. Nothing is stored.
|
|
51
55
|
- [Go live](https://github.com/vocalstack/python-sdk/blob/main/docs/go-live.md): Publish live audio to a stored transcription, and put it on a broadcast link listeners open. Control is HTTP; audio is one WebSocket.
|
|
@@ -65,6 +69,7 @@ pip install vocalstack
|
|
|
65
69
|
| transcription | a file, link, microphone or stream, transcribed and stored | `vs.transcriptions` |
|
|
66
70
|
| recording | one take inside a broadcast or a transcription | `vs.recordings` |
|
|
67
71
|
| job | a file or link recording's transcription work | `vs.jobs` |
|
|
72
|
+
| batch | many files transcribed together, for less than one transcription each | `vs.batches` |
|
|
68
73
|
|
|
69
74
|
The other groups: `vs.corrections`, `vs.share_links`, `vs.voices`,
|
|
70
75
|
`vs.search`, `vs.me`, `vs.languages`, `vs.rtmp`, `vs.events`, `vs.meta`.
|
|
@@ -5,6 +5,8 @@ The VocalStack speech API for Python (3.10 or newer, asyncio): one client,
|
|
|
5
5
|
are pydantic models generated from the API's one OpenAPI document
|
|
6
6
|
(`/v1/openapi.json`), with snake_case attributes.
|
|
7
7
|
|
|
8
|
+
Read more about the [VocalStack Python SDK](https://vocalstack.com/#developers) and our other SDKs on the [VocalStack website](https://vocalstack.com/).
|
|
9
|
+
|
|
8
10
|
```python
|
|
9
11
|
import asyncio, os
|
|
10
12
|
from vocalstack import VocalStack
|
|
@@ -32,7 +34,9 @@ pip install vocalstack
|
|
|
32
34
|
|
|
33
35
|
- [Quickstart](https://github.com/vocalstack/python-sdk/blob/main/docs/quickstart.md): An API key, the SDK, and your first transcript in a dozen lines.
|
|
34
36
|
- [Transcribe a file](https://github.com/vocalstack/python-sdk/blob/main/docs/transcribe-a-file.md): Upload a file, wait for its job, read the transcript and export it. Asynchronous; nothing to keep open.
|
|
35
|
-
- [Transcribe a link](https://github.com/vocalstack/python-sdk/blob/main/docs/transcribe-a-link.md): The server fetches a public URL or a
|
|
37
|
+
- [Transcribe a link](https://github.com/vocalstack/python-sdk/blob/main/docs/transcribe-a-link.md): The server fetches a public URL, a YouTube video or a recording on a video or audio site, then transcribes it like a file. Asynchronous, with a fetch phase first.
|
|
38
|
+
- [Transcribe a batch of files](https://github.com/vocalstack/python-sdk/blob/main/docs/transcribe-a-batch.md): Send many files or links as one batch, start it once, and read every transcript when it is done. Cheaper than one transcription per file.
|
|
39
|
+
- [Search your transcriptions](https://github.com/vocalstack/python-sdk/blob/main/docs/search-transcriptions.md): Find transcriptions by words in their name or link, site, language, kind, tag and date, keep the shared ones, and play each with its share code.
|
|
36
40
|
- [Stream a microphone](https://github.com/vocalstack/python-sdk/blob/main/docs/stream-a-microphone.md): Live audio in, transcript events out, over one WebSocket. Drafts replace the live tail; commits append.
|
|
37
41
|
- [Stream a file at full speed](https://github.com/vocalstack/python-sdk/blob/main/docs/stream-a-file.md): Send a file's bytes through a realtime session and read the transcript as fast as the server makes it. Nothing is stored.
|
|
38
42
|
- [Go live](https://github.com/vocalstack/python-sdk/blob/main/docs/go-live.md): Publish live audio to a stored transcription, and put it on a broadcast link listeners open. Control is HTTP; audio is one WebSocket.
|
|
@@ -52,6 +56,7 @@ pip install vocalstack
|
|
|
52
56
|
| transcription | a file, link, microphone or stream, transcribed and stored | `vs.transcriptions` |
|
|
53
57
|
| recording | one take inside a broadcast or a transcription | `vs.recordings` |
|
|
54
58
|
| job | a file or link recording's transcription work | `vs.jobs` |
|
|
59
|
+
| batch | many files transcribed together, for less than one transcription each | `vs.batches` |
|
|
55
60
|
|
|
56
61
|
The other groups: `vs.corrections`, `vs.share_links`, `vs.voices`,
|
|
57
62
|
`vs.search`, `vs.me`, `vs.languages`, `vs.rtmp`, `vs.events`, `vs.meta`.
|
|
@@ -20,6 +20,9 @@ labels and translation. The `vocalstack` package has one asyncio client,
|
|
|
20
20
|
- A **job** is a file or link recording's transcription work:
|
|
21
21
|
`await vs.jobs.wait(recording_id)` polls it until it is `done` or `failed`
|
|
22
22
|
(`on_progress=lambda job: ...` sees each read: `job.state`, `job.progress.pct`).
|
|
23
|
+
- A **batch** is many files transcribed together for less than one
|
|
24
|
+
transcription each: `vs.batches.create()`, `add_file`/`add_url` per file,
|
|
25
|
+
`start`, then `wait` (section 6).
|
|
23
26
|
- A **broadcast** is a link listeners open to follow a live recording.
|
|
24
27
|
- A **realtime session** (`vs.realtime`) is a socket you stream audio into and
|
|
25
28
|
read the transcript from. Nothing is stored. "Session" means only this.
|
|
@@ -94,7 +97,9 @@ async def main(path): # mp3, wav, flac, ogg, m4a, mp4 ...
|
|
|
94
97
|
async with VocalStack(os.environ.get("VOCALSTACK_BASE_URL"), api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
|
|
95
98
|
tx = await vs.transcriptions.create(
|
|
96
99
|
file=path, # a path, bytes, or an open binary file
|
|
97
|
-
settings={"spokenLanguage": "en", "speakerLabels": "on"}, # both optional; "auto" | "on" | "off"
|
|
100
|
+
settings={"spokenLanguage": "en", "speakerLabels": "on"}, # both optional; "auto" (default) | "on" | "off"
|
|
101
|
+
# Off unless asked for: settings summary, title, summaryTranslations,
|
|
102
|
+
# voice (spoken translations), and share_link=True (a public share link).
|
|
98
103
|
)
|
|
99
104
|
job = await vs.jobs.wait(tx.job.recording_id)
|
|
100
105
|
if job.state != "done":
|
|
@@ -180,6 +185,43 @@ async def main(q): # 2+ characters
|
|
|
180
185
|
asyncio.run(main(sys.argv[1]))
|
|
181
186
|
```
|
|
182
187
|
|
|
188
|
+
By meaning, in any language, even with none of the words (vector search). Each
|
|
189
|
+
hit's `passage` is its whole paragraph: give the passages to your own model as
|
|
190
|
+
context to answer questions about what was said (RAG). Pay as you go counts 60
|
|
191
|
+
an hour (`allowance`); Premium and Enterprise have no limit.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
res = await vs.search.vector("how much does housing cost")
|
|
195
|
+
for h in res.hits:
|
|
196
|
+
print(f"{h.similarity:.2f}", h.passage.media_start, h.passage.text)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## 6. A batch of files
|
|
200
|
+
|
|
201
|
+
Many files at once cost less as a batch: one create, one minimum charge for
|
|
202
|
+
the whole batch. Its files get transcripts, exports and speaker labels (when
|
|
203
|
+
`settings.speakerLabels` asks), never a summary, title, share link or voices.
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import asyncio, os, sys
|
|
207
|
+
from vocalstack import VocalStack
|
|
208
|
+
|
|
209
|
+
async def main(paths):
|
|
210
|
+
async with VocalStack(os.environ.get("VOCALSTACK_BASE_URL"), api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
|
|
211
|
+
batch = await vs.batches.create(name="Interviews", settings={"spokenLanguage": "en"})
|
|
212
|
+
for path in paths:
|
|
213
|
+
await vs.batches.add_file(batch.id, file=path) # or add_url(batch.id, url)
|
|
214
|
+
await vs.batches.start(batch.id)
|
|
215
|
+
done = await vs.batches.wait(batch.id) # done (or stopped); check done.counts.failed
|
|
216
|
+
for tx in done.files or []:
|
|
217
|
+
if tx.job and tx.job.state == "done":
|
|
218
|
+
snap = await vs.recordings.transcript(tx.job.recording_id)
|
|
219
|
+
print(tx.name, " ".join(s.text for s in snap.sentences))
|
|
220
|
+
# vs.batches.export(batch.id, "srt") is every finished transcript in one zip.
|
|
221
|
+
|
|
222
|
+
asyncio.run(main(sys.argv[1:]))
|
|
223
|
+
```
|
|
224
|
+
|
|
183
225
|
## Errors
|
|
184
226
|
|
|
185
227
|
A failed call raises `vocalstack.VocalStackError` with `.code`, `.message`,
|
|
@@ -215,7 +257,8 @@ except VocalStackError as e:
|
|
|
215
257
|
- The other groups: `vs.transcriptions` (`list`, `get`, `update`, `archive`,
|
|
216
258
|
`restore`, `purge`, `list_share_links`, `create_share_link`), `vs.recordings`
|
|
217
259
|
(`get`, `summarize`, `summary`, `translation`, `audio_url`, `subscribe`),
|
|
218
|
-
`vs.
|
|
260
|
+
`vs.batches` (`create`, `add_file`, `add_url`, `start`, `wait`, `get`, `list`,
|
|
261
|
+
`export`, `stop`, `delete`), `vs.corrections`, `vs.share_links`, `vs.voices`, `vs.broadcasts`,
|
|
219
262
|
`vs.me` (`get`, `usage`), `vs.languages`, `vs.rtmp`, `vs.events`, `vs.meta`.
|
|
220
263
|
Each method is its operation's name in the OpenAPI document, in snake_case
|
|
221
264
|
(`transcriptions.create` is `POST /v1/transcriptions`).
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vocalstack"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0a2"
|
|
8
8
|
description = "VocalStack speech API SDK (asyncio): one client grouped by resource, typed models from the API contract, realtime WebSocket streaming."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -22,11 +22,8 @@ Homepage = "https://vocalstack.com"
|
|
|
22
22
|
[tool.hatch.build.targets.wheel]
|
|
23
23
|
packages = ["src/vocalstack"]
|
|
24
24
|
|
|
25
|
-
# The Agent Skill ships inside the package (site-packages/vocalstack/SKILL.md)
|
|
26
|
-
# so an agent can find it next to the code.
|
|
27
25
|
[tool.hatch.build.targets.wheel.force-include]
|
|
28
26
|
"SKILL.md" = "vocalstack/SKILL.md"
|
|
29
27
|
|
|
30
|
-
# An explicit list: nothing else from the repository goes into the sdist.
|
|
31
28
|
[tool.hatch.build.targets.sdist]
|
|
32
29
|
only-include = ["src/vocalstack", "LICENSE", "README.md", "SKILL.md", "skills", "pyproject.toml"]
|
|
@@ -20,6 +20,9 @@ labels and translation. The `vocalstack` package has one asyncio client,
|
|
|
20
20
|
- A **job** is a file or link recording's transcription work:
|
|
21
21
|
`await vs.jobs.wait(recording_id)` polls it until it is `done` or `failed`
|
|
22
22
|
(`on_progress=lambda job: ...` sees each read: `job.state`, `job.progress.pct`).
|
|
23
|
+
- A **batch** is many files transcribed together for less than one
|
|
24
|
+
transcription each: `vs.batches.create()`, `add_file`/`add_url` per file,
|
|
25
|
+
`start`, then `wait` (section 6).
|
|
23
26
|
- A **broadcast** is a link listeners open to follow a live recording.
|
|
24
27
|
- A **realtime session** (`vs.realtime`) is a socket you stream audio into and
|
|
25
28
|
read the transcript from. Nothing is stored. "Session" means only this.
|
|
@@ -94,7 +97,9 @@ async def main(path): # mp3, wav, flac, ogg, m4a, mp4 ...
|
|
|
94
97
|
async with VocalStack(os.environ.get("VOCALSTACK_BASE_URL"), api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
|
|
95
98
|
tx = await vs.transcriptions.create(
|
|
96
99
|
file=path, # a path, bytes, or an open binary file
|
|
97
|
-
settings={"spokenLanguage": "en", "speakerLabels": "on"}, # both optional; "auto" | "on" | "off"
|
|
100
|
+
settings={"spokenLanguage": "en", "speakerLabels": "on"}, # both optional; "auto" (default) | "on" | "off"
|
|
101
|
+
# Off unless asked for: settings summary, title, summaryTranslations,
|
|
102
|
+
# voice (spoken translations), and share_link=True (a public share link).
|
|
98
103
|
)
|
|
99
104
|
job = await vs.jobs.wait(tx.job.recording_id)
|
|
100
105
|
if job.state != "done":
|
|
@@ -180,6 +185,43 @@ async def main(q): # 2+ characters
|
|
|
180
185
|
asyncio.run(main(sys.argv[1]))
|
|
181
186
|
```
|
|
182
187
|
|
|
188
|
+
By meaning, in any language, even with none of the words (vector search). Each
|
|
189
|
+
hit's `passage` is its whole paragraph: give the passages to your own model as
|
|
190
|
+
context to answer questions about what was said (RAG). Pay as you go counts 60
|
|
191
|
+
an hour (`allowance`); Premium and Enterprise have no limit.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
res = await vs.search.vector("how much does housing cost")
|
|
195
|
+
for h in res.hits:
|
|
196
|
+
print(f"{h.similarity:.2f}", h.passage.media_start, h.passage.text)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## 6. A batch of files
|
|
200
|
+
|
|
201
|
+
Many files at once cost less as a batch: one create, one minimum charge for
|
|
202
|
+
the whole batch. Its files get transcripts, exports and speaker labels (when
|
|
203
|
+
`settings.speakerLabels` asks), never a summary, title, share link or voices.
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import asyncio, os, sys
|
|
207
|
+
from vocalstack import VocalStack
|
|
208
|
+
|
|
209
|
+
async def main(paths):
|
|
210
|
+
async with VocalStack(os.environ.get("VOCALSTACK_BASE_URL"), api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
|
|
211
|
+
batch = await vs.batches.create(name="Interviews", settings={"spokenLanguage": "en"})
|
|
212
|
+
for path in paths:
|
|
213
|
+
await vs.batches.add_file(batch.id, file=path) # or add_url(batch.id, url)
|
|
214
|
+
await vs.batches.start(batch.id)
|
|
215
|
+
done = await vs.batches.wait(batch.id) # done (or stopped); check done.counts.failed
|
|
216
|
+
for tx in done.files or []:
|
|
217
|
+
if tx.job and tx.job.state == "done":
|
|
218
|
+
snap = await vs.recordings.transcript(tx.job.recording_id)
|
|
219
|
+
print(tx.name, " ".join(s.text for s in snap.sentences))
|
|
220
|
+
# vs.batches.export(batch.id, "srt") is every finished transcript in one zip.
|
|
221
|
+
|
|
222
|
+
asyncio.run(main(sys.argv[1:]))
|
|
223
|
+
```
|
|
224
|
+
|
|
183
225
|
## Errors
|
|
184
226
|
|
|
185
227
|
A failed call raises `vocalstack.VocalStackError` with `.code`, `.message`,
|
|
@@ -215,7 +257,8 @@ except VocalStackError as e:
|
|
|
215
257
|
- The other groups: `vs.transcriptions` (`list`, `get`, `update`, `archive`,
|
|
216
258
|
`restore`, `purge`, `list_share_links`, `create_share_link`), `vs.recordings`
|
|
217
259
|
(`get`, `summarize`, `summary`, `translation`, `audio_url`, `subscribe`),
|
|
218
|
-
`vs.
|
|
260
|
+
`vs.batches` (`create`, `add_file`, `add_url`, `start`, `wait`, `get`, `list`,
|
|
261
|
+
`export`, `stop`, `delete`), `vs.corrections`, `vs.share_links`, `vs.voices`, `vs.broadcasts`,
|
|
219
262
|
`vs.me` (`get`, `usage`), `vs.languages`, `vs.rtmp`, `vs.events`, `vs.meta`.
|
|
220
263
|
Each method is its operation's name in the OpenAPI document, in snake_case
|
|
221
264
|
(`transcriptions.create` is `POST /v1/transcriptions`).
|
|
@@ -8,7 +8,7 @@ from typing import Any, Callable, Dict, Mapping, Optional, Union
|
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
|
|
11
|
-
from .errors import from_envelope, from_oauth
|
|
11
|
+
from .errors import VocalStackError, from_envelope, from_oauth
|
|
12
12
|
|
|
13
13
|
DEFAULT_BASE_URL = "https://api.vocalstack.com"
|
|
14
14
|
|
|
@@ -30,7 +30,6 @@ def listener_query(listener_token: Optional[str], share_code: Optional[str]) ->
|
|
|
30
30
|
return {"listenerToken": listener_token, "shareCode": share_code}
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
#: The wait before retrying ``503 unavailable`` when it names none.
|
|
34
33
|
UNAVAILABLE_WAIT_S = 2.0
|
|
35
34
|
|
|
36
35
|
|
|
@@ -120,16 +119,19 @@ class Transport:
|
|
|
120
119
|
h["idempotency-key"] = idempotency_key
|
|
121
120
|
attempt = 0
|
|
122
121
|
while True:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
122
|
+
try:
|
|
123
|
+
resp = await self.http.request(
|
|
124
|
+
method,
|
|
125
|
+
self.url(path),
|
|
126
|
+
params=_clean(query),
|
|
127
|
+
json=json_body,
|
|
128
|
+
files=files,
|
|
129
|
+
data=data,
|
|
130
|
+
headers=h,
|
|
131
|
+
follow_redirects=False,
|
|
132
|
+
)
|
|
133
|
+
except httpx.TransportError as e:
|
|
134
|
+
raise VocalStackError("unavailable", "could not reach the VocalStack API", retryable=True) from e
|
|
133
135
|
if resp.status_code < 400:
|
|
134
136
|
return resp
|
|
135
137
|
body = None
|
|
@@ -152,7 +154,12 @@ class Transport:
|
|
|
152
154
|
resp = await self.raw(method, path, **kw)
|
|
153
155
|
if 300 <= resp.status_code < 400:
|
|
154
156
|
return {"location": resp.headers.get("location")}
|
|
155
|
-
|
|
157
|
+
if not resp.content:
|
|
158
|
+
return None
|
|
159
|
+
try:
|
|
160
|
+
return resp.json()
|
|
161
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
162
|
+
raise VocalStackError("internal", "the API answered with a body that is not JSON", status=resp.status_code, headers=resp.headers) from e
|
|
156
163
|
|
|
157
164
|
async def aclose(self) -> None:
|
|
158
165
|
if self.own_http:
|