breeze-blue 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.
- breeze_blue-0.1.0/CHANGELOG.md +10 -0
- breeze_blue-0.1.0/LICENSE +21 -0
- breeze_blue-0.1.0/MANIFEST.in +5 -0
- breeze_blue-0.1.0/PKG-INFO +131 -0
- breeze_blue-0.1.0/README.md +109 -0
- breeze_blue-0.1.0/pyproject.toml +55 -0
- breeze_blue-0.1.0/setup.cfg +4 -0
- breeze_blue-0.1.0/src/breeze_blue/__init__.py +72 -0
- breeze_blue-0.1.0/src/breeze_blue/_audio.py +118 -0
- breeze_blue-0.1.0/src/breeze_blue/_client.py +126 -0
- breeze_blue-0.1.0/src/breeze_blue/_errors.py +136 -0
- breeze_blue-0.1.0/src/breeze_blue/_resources.py +429 -0
- breeze_blue-0.1.0/src/breeze_blue/client.py +5 -0
- breeze_blue-0.1.0/src/breeze_blue/types.py +178 -0
- breeze_blue-0.1.0/src/breeze_blue.egg-info/SOURCES.txt +12 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- ElevenLabs-compatible client surface for text-to-speech, voices, text-to-voice, history, models, and account APIs.
|
|
8
|
+
- Typed errors for authentication, rate limiting, validation, billing, upstream, and service-unavailable failures.
|
|
9
|
+
- Audio playback helpers using ffplay, afplay on macOS, and mpv for streaming-style playback.
|
|
10
|
+
- `xi-api-key` auth header and `x-breeze-sdk` telemetry header.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Breeze Blue
|
|
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,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: breeze-blue
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Breeze Blue Developer API.
|
|
5
|
+
Author-email: Breeze Blue <support@breeze.blue>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://breeze.blue
|
|
8
|
+
Project-URL: Documentation, https://breeze.blue/docs
|
|
9
|
+
Keywords: audio,breeze,breeze-blue,elevenlabs,elevenlabs-alternative,sdk,text-to-speech,tts,voice
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: httpx<1.0.0,>=0.28.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Breeze Blue Python SDK
|
|
24
|
+
|
|
25
|
+
Local v0.1 Python SDK for the Breeze Blue Developer API.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
Before the package is published to PyPI, install it from this monorepo:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv sync
|
|
33
|
+
uv pip install -e ./packages/breeze-python
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
After the first public release, install the same SDK from PyPI:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add breeze-blue
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from breeze_blue import BreezeBlue, play, save, stream
|
|
44
|
+
|
|
45
|
+
client = BreezeBlue() # Reads BREEZE_API_KEY and defaults to https://api.breeze.blue
|
|
46
|
+
|
|
47
|
+
audio = client.text_to_speech.convert(
|
|
48
|
+
"voc_demo",
|
|
49
|
+
text="Hello from Breeze Blue.",
|
|
50
|
+
output_format="mp3_44100_128",
|
|
51
|
+
)
|
|
52
|
+
play(audio)
|
|
53
|
+
save(audio, "hello.mp3")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Client
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
client = BreezeBlue(
|
|
60
|
+
api_key="brz_secret",
|
|
61
|
+
base_url="https://api.breeze.blue",
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Authentication uses the ElevenLabs-compatible `xi-api-key` header to make migration easier. If `api_key` is omitted, the SDK reads `BREEZE_API_KEY`. If `base_url` is omitted, the SDK reads `BREEZE_BASE_URL` and then falls back to `https://api.breeze.blue`.
|
|
66
|
+
|
|
67
|
+
For local development, pass `base_url="http://127.0.0.1:8000"` or set `BREEZE_BASE_URL`.
|
|
68
|
+
|
|
69
|
+
Set a default client timeout with `BreezeBlue(timeout=120.0)`, or override a single call with `timeout=120.0`.
|
|
70
|
+
|
|
71
|
+
## Resources
|
|
72
|
+
|
|
73
|
+
- `client.text_to_speech.convert(voice_id, text=...)`
|
|
74
|
+
- `client.text_to_speech.stream(voice_id, text=...)`
|
|
75
|
+
- `client.voices.search(...)`
|
|
76
|
+
- `client.voices.get(voice_id)`
|
|
77
|
+
- `client.voices.ivc.create_preview(name=..., file=...)`
|
|
78
|
+
- `client.voices.ivc.edit(voice_id, name=..., file=...)`
|
|
79
|
+
- `client.voices.ivc.get_settings(voice_id)` / `client.voices.ivc.update_settings(voice_id, guidance_scale=...)`
|
|
80
|
+
- `client.voices.ivc.delete(voice_id)`
|
|
81
|
+
- `client.text_to_voice.design(voice_description=...)`
|
|
82
|
+
- `client.text_to_voice.create(generated_voice_id=..., voice_name=...)`
|
|
83
|
+
- `client.text_to_voice.stream_preview(generated_voice_id)`
|
|
84
|
+
- `client.history.list(...)`
|
|
85
|
+
- `client.history.get(history_item_id)`
|
|
86
|
+
- `client.history.download_audio(history_item_id)`
|
|
87
|
+
- `client.history.delete(history_item_id)`
|
|
88
|
+
- `client.models.list()`
|
|
89
|
+
- `client.account.balance()`
|
|
90
|
+
- `client.account.usage(...)`
|
|
91
|
+
|
|
92
|
+
All resource methods accept `timeout=<seconds>` to override the client default for a single call.
|
|
93
|
+
Common response shapes are exported as `TypedDict` types from `breeze_blue.types` and from the package root.
|
|
94
|
+
|
|
95
|
+
## Audio Helpers
|
|
96
|
+
|
|
97
|
+
Audio endpoints return `AudioResponse` with:
|
|
98
|
+
|
|
99
|
+
- `content`
|
|
100
|
+
- `content_type`
|
|
101
|
+
- `history_item_id`
|
|
102
|
+
|
|
103
|
+
Helpers:
|
|
104
|
+
|
|
105
|
+
- `audio.save(path)`
|
|
106
|
+
- `audio.play()` uses `ffplay`, with macOS `afplay` fallback, and waits for playback to finish.
|
|
107
|
+
- `audio.stream()` pipes the buffered response content to `mpv` and waits for playback to finish.
|
|
108
|
+
- `play(audio)`, `save(audio, path)`, and `stream(audio)` are also exported
|
|
109
|
+
from `breeze_blue` for ElevenLabs-style examples.
|
|
110
|
+
- `stream(audio)` also accepts an iterable of byte chunks for chunked playback.
|
|
111
|
+
- `start_play(audio)` and `start_stream(audio)` are available for advanced
|
|
112
|
+
fire-and-forget process control.
|
|
113
|
+
|
|
114
|
+
## Errors
|
|
115
|
+
|
|
116
|
+
All API errors inherit from `BreezeBlueError`. HTTP and Breeze error codes are mapped to typed exceptions including `BadRequestError`, `AuthenticationError`, `ForbiddenError`, `NotFoundError`, `ConflictError`, `ValidationError`, `RateLimitError`, `BillingInsufficientCreditsError`, `UpstreamError`, and `ServiceUnavailableError`.
|
|
117
|
+
|
|
118
|
+
## Local release workflow
|
|
119
|
+
|
|
120
|
+
The Python and TypeScript SDKs share one SemVer version. From the monorepo root:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
mise exec -- pnpm sdk:status
|
|
124
|
+
mise exec -- pnpm sdk:version patch
|
|
125
|
+
mise exec -- pnpm sdk:check
|
|
126
|
+
mise exec -- pnpm sdk:build
|
|
127
|
+
mise exec -- pnpm sdk:smoke
|
|
128
|
+
mise exec -- pnpm sdk:publish -- --dry-run
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use `patch` for compatible bug fixes, `minor` for compatible new features, and `major` only for breaking public API changes. Actual PyPI publishing uses local `uv publish` credentials, such as `UV_PUBLISH_TOKEN`; do not store tokens in the repo.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Breeze Blue Python SDK
|
|
2
|
+
|
|
3
|
+
Local v0.1 Python SDK for the Breeze Blue Developer API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Before the package is published to PyPI, install it from this monorepo:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv sync
|
|
11
|
+
uv pip install -e ./packages/breeze-python
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
After the first public release, install the same SDK from PyPI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv add breeze-blue
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from breeze_blue import BreezeBlue, play, save, stream
|
|
22
|
+
|
|
23
|
+
client = BreezeBlue() # Reads BREEZE_API_KEY and defaults to https://api.breeze.blue
|
|
24
|
+
|
|
25
|
+
audio = client.text_to_speech.convert(
|
|
26
|
+
"voc_demo",
|
|
27
|
+
text="Hello from Breeze Blue.",
|
|
28
|
+
output_format="mp3_44100_128",
|
|
29
|
+
)
|
|
30
|
+
play(audio)
|
|
31
|
+
save(audio, "hello.mp3")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Client
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
client = BreezeBlue(
|
|
38
|
+
api_key="brz_secret",
|
|
39
|
+
base_url="https://api.breeze.blue",
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Authentication uses the ElevenLabs-compatible `xi-api-key` header to make migration easier. If `api_key` is omitted, the SDK reads `BREEZE_API_KEY`. If `base_url` is omitted, the SDK reads `BREEZE_BASE_URL` and then falls back to `https://api.breeze.blue`.
|
|
44
|
+
|
|
45
|
+
For local development, pass `base_url="http://127.0.0.1:8000"` or set `BREEZE_BASE_URL`.
|
|
46
|
+
|
|
47
|
+
Set a default client timeout with `BreezeBlue(timeout=120.0)`, or override a single call with `timeout=120.0`.
|
|
48
|
+
|
|
49
|
+
## Resources
|
|
50
|
+
|
|
51
|
+
- `client.text_to_speech.convert(voice_id, text=...)`
|
|
52
|
+
- `client.text_to_speech.stream(voice_id, text=...)`
|
|
53
|
+
- `client.voices.search(...)`
|
|
54
|
+
- `client.voices.get(voice_id)`
|
|
55
|
+
- `client.voices.ivc.create_preview(name=..., file=...)`
|
|
56
|
+
- `client.voices.ivc.edit(voice_id, name=..., file=...)`
|
|
57
|
+
- `client.voices.ivc.get_settings(voice_id)` / `client.voices.ivc.update_settings(voice_id, guidance_scale=...)`
|
|
58
|
+
- `client.voices.ivc.delete(voice_id)`
|
|
59
|
+
- `client.text_to_voice.design(voice_description=...)`
|
|
60
|
+
- `client.text_to_voice.create(generated_voice_id=..., voice_name=...)`
|
|
61
|
+
- `client.text_to_voice.stream_preview(generated_voice_id)`
|
|
62
|
+
- `client.history.list(...)`
|
|
63
|
+
- `client.history.get(history_item_id)`
|
|
64
|
+
- `client.history.download_audio(history_item_id)`
|
|
65
|
+
- `client.history.delete(history_item_id)`
|
|
66
|
+
- `client.models.list()`
|
|
67
|
+
- `client.account.balance()`
|
|
68
|
+
- `client.account.usage(...)`
|
|
69
|
+
|
|
70
|
+
All resource methods accept `timeout=<seconds>` to override the client default for a single call.
|
|
71
|
+
Common response shapes are exported as `TypedDict` types from `breeze_blue.types` and from the package root.
|
|
72
|
+
|
|
73
|
+
## Audio Helpers
|
|
74
|
+
|
|
75
|
+
Audio endpoints return `AudioResponse` with:
|
|
76
|
+
|
|
77
|
+
- `content`
|
|
78
|
+
- `content_type`
|
|
79
|
+
- `history_item_id`
|
|
80
|
+
|
|
81
|
+
Helpers:
|
|
82
|
+
|
|
83
|
+
- `audio.save(path)`
|
|
84
|
+
- `audio.play()` uses `ffplay`, with macOS `afplay` fallback, and waits for playback to finish.
|
|
85
|
+
- `audio.stream()` pipes the buffered response content to `mpv` and waits for playback to finish.
|
|
86
|
+
- `play(audio)`, `save(audio, path)`, and `stream(audio)` are also exported
|
|
87
|
+
from `breeze_blue` for ElevenLabs-style examples.
|
|
88
|
+
- `stream(audio)` also accepts an iterable of byte chunks for chunked playback.
|
|
89
|
+
- `start_play(audio)` and `start_stream(audio)` are available for advanced
|
|
90
|
+
fire-and-forget process control.
|
|
91
|
+
|
|
92
|
+
## Errors
|
|
93
|
+
|
|
94
|
+
All API errors inherit from `BreezeBlueError`. HTTP and Breeze error codes are mapped to typed exceptions including `BadRequestError`, `AuthenticationError`, `ForbiddenError`, `NotFoundError`, `ConflictError`, `ValidationError`, `RateLimitError`, `BillingInsufficientCreditsError`, `UpstreamError`, and `ServiceUnavailableError`.
|
|
95
|
+
|
|
96
|
+
## Local release workflow
|
|
97
|
+
|
|
98
|
+
The Python and TypeScript SDKs share one SemVer version. From the monorepo root:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
mise exec -- pnpm sdk:status
|
|
102
|
+
mise exec -- pnpm sdk:version patch
|
|
103
|
+
mise exec -- pnpm sdk:check
|
|
104
|
+
mise exec -- pnpm sdk:build
|
|
105
|
+
mise exec -- pnpm sdk:smoke
|
|
106
|
+
mise exec -- pnpm sdk:publish -- --dry-run
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Use `patch` for compatible bug fixes, `minor` for compatible new features, and `major` only for breaking public API changes. Actual PyPI publishing uses local `uv publish` credentials, such as `UV_PUBLISH_TOKEN`; do not store tokens in the repo.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "breeze-blue"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Breeze Blue Developer API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Breeze Blue", email = "support@breeze.blue" },
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"audio",
|
|
18
|
+
"breeze",
|
|
19
|
+
"breeze-blue",
|
|
20
|
+
"elevenlabs",
|
|
21
|
+
"elevenlabs-alternative",
|
|
22
|
+
"sdk",
|
|
23
|
+
"text-to-speech",
|
|
24
|
+
"tts",
|
|
25
|
+
"voice",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"httpx>=0.28.0,<1.0.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://breeze.blue"
|
|
42
|
+
Documentation = "https://breeze.blue/docs"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"exceptiongroup>=1.2.0,<2.0.0; python_version < '3.11'",
|
|
47
|
+
"pyright>=1.1.407,<2.0.0",
|
|
48
|
+
"pytest>=8.4.2,<9.0.0",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
pythonpath = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["src"]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from ._audio import AudioResponse, play, save, start_play, start_stream, stream
|
|
6
|
+
from ._client import BreezeBlue
|
|
7
|
+
from ._errors import (
|
|
8
|
+
APIError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
BadRequestError,
|
|
11
|
+
BillingInsufficientCreditsError,
|
|
12
|
+
BreezeBlueError,
|
|
13
|
+
ConflictError,
|
|
14
|
+
ForbiddenError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
ServiceUnavailableError,
|
|
18
|
+
UpstreamError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
)
|
|
21
|
+
from .types import (
|
|
22
|
+
Balance,
|
|
23
|
+
GenericStatus,
|
|
24
|
+
HistoryItem,
|
|
25
|
+
HistoryList,
|
|
26
|
+
Model,
|
|
27
|
+
Usage,
|
|
28
|
+
Voice,
|
|
29
|
+
VoiceClonePreview,
|
|
30
|
+
VoiceDesignResponse,
|
|
31
|
+
VoiceList,
|
|
32
|
+
VoiceSettings,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
__version__ = version("breeze-blue")
|
|
37
|
+
except PackageNotFoundError:
|
|
38
|
+
__version__ = "0.0.0"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"APIError",
|
|
42
|
+
"AudioResponse",
|
|
43
|
+
"AuthenticationError",
|
|
44
|
+
"BadRequestError",
|
|
45
|
+
"BillingInsufficientCreditsError",
|
|
46
|
+
"BreezeBlue",
|
|
47
|
+
"BreezeBlueError",
|
|
48
|
+
"ConflictError",
|
|
49
|
+
"ForbiddenError",
|
|
50
|
+
"NotFoundError",
|
|
51
|
+
"RateLimitError",
|
|
52
|
+
"ServiceUnavailableError",
|
|
53
|
+
"UpstreamError",
|
|
54
|
+
"ValidationError",
|
|
55
|
+
"Balance",
|
|
56
|
+
"GenericStatus",
|
|
57
|
+
"HistoryItem",
|
|
58
|
+
"HistoryList",
|
|
59
|
+
"Model",
|
|
60
|
+
"Usage",
|
|
61
|
+
"Voice",
|
|
62
|
+
"VoiceClonePreview",
|
|
63
|
+
"VoiceDesignResponse",
|
|
64
|
+
"VoiceList",
|
|
65
|
+
"VoiceSettings",
|
|
66
|
+
"__version__",
|
|
67
|
+
"play",
|
|
68
|
+
"save",
|
|
69
|
+
"start_play",
|
|
70
|
+
"start_stream",
|
|
71
|
+
"stream",
|
|
72
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlayerNotFoundError(RuntimeError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class AudioResponse:
|
|
17
|
+
content: bytes
|
|
18
|
+
content_type: str | None = None
|
|
19
|
+
history_item_id: str | None = None
|
|
20
|
+
|
|
21
|
+
def save(self, path: str | Path) -> Path:
|
|
22
|
+
target = Path(path)
|
|
23
|
+
target.write_bytes(self.content)
|
|
24
|
+
return target
|
|
25
|
+
|
|
26
|
+
def play(self) -> None:
|
|
27
|
+
ffplay = shutil.which("ffplay")
|
|
28
|
+
if ffplay:
|
|
29
|
+
_pipe_to_player([ffplay, "-autoexit", "-nodisp", "-"], self.content)
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
if platform.system() == "Darwin":
|
|
33
|
+
afplay = shutil.which("afplay")
|
|
34
|
+
if afplay:
|
|
35
|
+
_pipe_to_player([afplay, "-"], self.content)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
raise PlayerNotFoundError("Install ffplay, or afplay on macOS, to play audio.")
|
|
39
|
+
|
|
40
|
+
def stream(self) -> None:
|
|
41
|
+
mpv = shutil.which("mpv")
|
|
42
|
+
if not mpv:
|
|
43
|
+
raise PlayerNotFoundError("Install mpv to stream audio.")
|
|
44
|
+
_pipe_to_player([mpv, "--no-terminal", "-"], self.content)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save(audio: AudioResponse | bytes | bytearray, path: str | Path) -> Path:
|
|
48
|
+
return _coerce_audio_response(audio).save(path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def play(audio: AudioResponse | bytes | bytearray) -> None:
|
|
52
|
+
return _coerce_audio_response(audio).play()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def stream(audio: AudioResponse | bytes | bytearray | Iterable[bytes]) -> None:
|
|
56
|
+
mpv = shutil.which("mpv")
|
|
57
|
+
if not mpv:
|
|
58
|
+
raise PlayerNotFoundError("Install mpv to stream audio.")
|
|
59
|
+
_pipe_chunks_to_player([mpv, "--no-terminal", "-"], _iter_audio_chunks(audio))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def start_play(audio: AudioResponse | bytes | bytearray) -> subprocess.Popen[bytes]:
|
|
63
|
+
response = _coerce_audio_response(audio)
|
|
64
|
+
ffplay = shutil.which("ffplay")
|
|
65
|
+
if ffplay:
|
|
66
|
+
return _start_player([ffplay, "-autoexit", "-nodisp", "-"], response.content)
|
|
67
|
+
if platform.system() == "Darwin":
|
|
68
|
+
afplay = shutil.which("afplay")
|
|
69
|
+
if afplay:
|
|
70
|
+
return _start_player([afplay, "-"], response.content)
|
|
71
|
+
raise PlayerNotFoundError("Install ffplay, or afplay on macOS, to play audio.")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def start_stream(audio: AudioResponse | bytes | bytearray | Iterable[bytes]) -> subprocess.Popen[bytes]:
|
|
75
|
+
mpv = shutil.which("mpv")
|
|
76
|
+
if not mpv:
|
|
77
|
+
raise PlayerNotFoundError("Install mpv to stream audio.")
|
|
78
|
+
return _start_player_with_chunks([mpv, "--no-terminal", "-"], _iter_audio_chunks(audio))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _coerce_audio_response(audio: AudioResponse | bytes | bytearray) -> AudioResponse:
|
|
82
|
+
if isinstance(audio, AudioResponse):
|
|
83
|
+
return audio
|
|
84
|
+
return AudioResponse(content=bytes(audio))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _iter_audio_chunks(audio: AudioResponse | bytes | bytearray | Iterable[bytes]) -> Iterable[bytes]:
|
|
88
|
+
if isinstance(audio, AudioResponse):
|
|
89
|
+
yield audio.content
|
|
90
|
+
return
|
|
91
|
+
if isinstance(audio, bytes | bytearray):
|
|
92
|
+
yield bytes(audio)
|
|
93
|
+
return
|
|
94
|
+
yield from audio
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _pipe_to_player(command: list[str], audio: bytes) -> None:
|
|
98
|
+
process = _start_player(command, audio)
|
|
99
|
+
process.wait()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _pipe_chunks_to_player(command: list[str], chunks: Iterable[bytes]) -> None:
|
|
103
|
+
process = _start_player_with_chunks(command, chunks)
|
|
104
|
+
process.wait()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _start_player(command: list[str], audio: bytes) -> subprocess.Popen[bytes]:
|
|
108
|
+
return _start_player_with_chunks(command, [audio])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _start_player_with_chunks(command: list[str], chunks: Iterable[bytes]) -> subprocess.Popen[bytes]:
|
|
112
|
+
process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
|
113
|
+
if process.stdin is not None:
|
|
114
|
+
for chunk in chunks:
|
|
115
|
+
if chunk:
|
|
116
|
+
process.stdin.write(chunk)
|
|
117
|
+
process.stdin.close()
|
|
118
|
+
return process
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._audio import AudioResponse
|
|
10
|
+
from ._errors import raise_for_response
|
|
11
|
+
from ._resources import AccountResource, HistoryResource, ModelsResource, TextToSpeechResource, TextToVoiceResource, VoicesResource
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://api.breeze.blue"
|
|
15
|
+
try:
|
|
16
|
+
SDK_VERSION = version("breeze-blue")
|
|
17
|
+
except PackageNotFoundError:
|
|
18
|
+
SDK_VERSION = "0.0.0"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BreezeBlue:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
api_key: str | None = None,
|
|
26
|
+
base_url: str | None = None,
|
|
27
|
+
timeout: float | httpx.Timeout = 60.0,
|
|
28
|
+
http_client: httpx.Client | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
self.api_key = api_key if api_key is not None else os.getenv("BREEZE_API_KEY")
|
|
31
|
+
resolved_base_url = base_url or os.getenv("BREEZE_BASE_URL") or DEFAULT_BASE_URL
|
|
32
|
+
self.base_url = resolved_base_url.rstrip("/")
|
|
33
|
+
self._owns_client = http_client is None
|
|
34
|
+
self._client = http_client or httpx.Client(timeout=timeout)
|
|
35
|
+
|
|
36
|
+
self.text_to_speech = TextToSpeechResource(self)
|
|
37
|
+
self.voices = VoicesResource(self)
|
|
38
|
+
self.text_to_voice = TextToVoiceResource(self)
|
|
39
|
+
self.history = HistoryResource(self)
|
|
40
|
+
self.models = ModelsResource(self)
|
|
41
|
+
self.account = AccountResource(self)
|
|
42
|
+
|
|
43
|
+
def close(self) -> None:
|
|
44
|
+
if self._owns_client:
|
|
45
|
+
self._client.close()
|
|
46
|
+
|
|
47
|
+
def __enter__(self) -> BreezeBlue:
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, *_exc: object) -> None:
|
|
51
|
+
self.close()
|
|
52
|
+
|
|
53
|
+
def request(
|
|
54
|
+
self,
|
|
55
|
+
method: str,
|
|
56
|
+
path: str,
|
|
57
|
+
*,
|
|
58
|
+
params: dict[str, Any] | None = None,
|
|
59
|
+
json: dict[str, Any] | None = None,
|
|
60
|
+
data: dict[str, Any] | None = None,
|
|
61
|
+
files: dict[str, Any] | list[tuple[str, Any]] | None = None,
|
|
62
|
+
timeout: float | httpx.Timeout | None = None,
|
|
63
|
+
) -> httpx.Response:
|
|
64
|
+
request_kwargs: dict[str, Any] = {
|
|
65
|
+
"params": _drop_none(params),
|
|
66
|
+
"json": json,
|
|
67
|
+
"data": _drop_none(data),
|
|
68
|
+
"files": files,
|
|
69
|
+
"headers": self._headers(),
|
|
70
|
+
}
|
|
71
|
+
if timeout is not None:
|
|
72
|
+
request_kwargs["timeout"] = timeout
|
|
73
|
+
response = self._client.request(method, self._url(path), **request_kwargs)
|
|
74
|
+
raise_for_response(response)
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
def json_request(
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
path: str,
|
|
81
|
+
*,
|
|
82
|
+
params: dict[str, Any] | None = None,
|
|
83
|
+
json: dict[str, Any] | None = None,
|
|
84
|
+
data: dict[str, Any] | None = None,
|
|
85
|
+
files: dict[str, Any] | list[tuple[str, Any]] | None = None,
|
|
86
|
+
timeout: float | httpx.Timeout | None = None,
|
|
87
|
+
) -> Any:
|
|
88
|
+
response = self.request(method, path, params=params, json=json, data=data, files=files, timeout=timeout)
|
|
89
|
+
if not response.content:
|
|
90
|
+
return None
|
|
91
|
+
return response.json()
|
|
92
|
+
|
|
93
|
+
def audio_request(
|
|
94
|
+
self,
|
|
95
|
+
method: str,
|
|
96
|
+
path: str,
|
|
97
|
+
*,
|
|
98
|
+
params: dict[str, Any] | None = None,
|
|
99
|
+
json: dict[str, Any] | None = None,
|
|
100
|
+
timeout: float | httpx.Timeout | None = None,
|
|
101
|
+
) -> AudioResponse:
|
|
102
|
+
response = self.request(method, path, params=params, json=json, timeout=timeout)
|
|
103
|
+
return AudioResponse(
|
|
104
|
+
content=response.content,
|
|
105
|
+
content_type=response.headers.get("content-type"),
|
|
106
|
+
history_item_id=response.headers.get("history-item-id"),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _url(self, path: str) -> str:
|
|
110
|
+
normalized = path if path.startswith("/") else f"/{path}"
|
|
111
|
+
return f"{self.base_url}{normalized}"
|
|
112
|
+
|
|
113
|
+
def _headers(self) -> dict[str, str]:
|
|
114
|
+
headers = {
|
|
115
|
+
"user-agent": f"breeze-blue-python/{SDK_VERSION}",
|
|
116
|
+
"x-breeze-sdk": f"breeze-blue-python/{SDK_VERSION}",
|
|
117
|
+
}
|
|
118
|
+
if self.api_key:
|
|
119
|
+
headers["xi-api-key"] = self.api_key
|
|
120
|
+
return headers
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _drop_none(payload: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
124
|
+
if payload is None:
|
|
125
|
+
return None
|
|
126
|
+
return {key: value for key, value in payload.items() if value is not None}
|