vocalbin 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.
- vocalbin-0.1.0/.env.example +1 -0
- vocalbin-0.1.0/.github/workflows/release.yml +109 -0
- vocalbin-0.1.0/.github/workflows/tests.yml +30 -0
- vocalbin-0.1.0/.gitignore +86 -0
- vocalbin-0.1.0/.pre-commit-config.yaml +18 -0
- vocalbin-0.1.0/.python-version +1 -0
- vocalbin-0.1.0/AGENTS.md +9 -0
- vocalbin-0.1.0/CONTRIBUTING.md +63 -0
- vocalbin-0.1.0/LICENSE +21 -0
- vocalbin-0.1.0/PKG-INFO +152 -0
- vocalbin-0.1.0/README.md +141 -0
- vocalbin-0.1.0/deployment.md +61 -0
- vocalbin-0.1.0/examples/output/.gitignore +2 -0
- vocalbin-0.1.0/examples/round_trip.py +49 -0
- vocalbin-0.1.0/examples/shared_client.py +45 -0
- vocalbin-0.1.0/examples/speech_to_text.py +154 -0
- vocalbin-0.1.0/examples/text_to_speech.py +121 -0
- vocalbin-0.1.0/pyproject.toml +35 -0
- vocalbin-0.1.0/static/banner.png +0 -0
- vocalbin-0.1.0/tests/test_clients.py +176 -0
- vocalbin-0.1.0/tests/test_credentials.py +39 -0
- vocalbin-0.1.0/tests/test_models.py +218 -0
- vocalbin-0.1.0/tests/test_ports.py +17 -0
- vocalbin-0.1.0/uv.lock +876 -0
- vocalbin-0.1.0/vision.md +55 -0
- vocalbin-0.1.0/vocalbin/__init__.py +33 -0
- vocalbin-0.1.0/vocalbin/clients.py +131 -0
- vocalbin-0.1.0/vocalbin/credentials.py +8 -0
- vocalbin-0.1.0/vocalbin/models.py +230 -0
- vocalbin-0.1.0/vocalbin/ports.py +18 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Check out repository
|
|
16
|
+
uses: actions/checkout@v7
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
|
|
20
|
+
- name: Install uv and Python
|
|
21
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
22
|
+
with:
|
|
23
|
+
version: "0.11.30"
|
|
24
|
+
python-version: "3.13"
|
|
25
|
+
enable-cache: true
|
|
26
|
+
|
|
27
|
+
- name: Verify release tag
|
|
28
|
+
shell: bash
|
|
29
|
+
run: |
|
|
30
|
+
git fetch origin main
|
|
31
|
+
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
|
|
32
|
+
tag_version="${GITHUB_REF_NAME#v}"
|
|
33
|
+
project_version="$(uv version --short)"
|
|
34
|
+
if [[ "$tag_version" != "$project_version" ]]; then
|
|
35
|
+
echo "Tag version $tag_version does not match project version $project_version."
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
- name: Install locked dependencies
|
|
40
|
+
run: uv sync --locked --dev --group release
|
|
41
|
+
|
|
42
|
+
- name: Run unit tests
|
|
43
|
+
run: uv run pytest
|
|
44
|
+
|
|
45
|
+
- name: Build distributions
|
|
46
|
+
run: uv build --out-dir dist
|
|
47
|
+
|
|
48
|
+
- name: Check distribution metadata
|
|
49
|
+
run: uv run twine check dist/*
|
|
50
|
+
|
|
51
|
+
- name: Smoke-test wheel
|
|
52
|
+
shell: bash
|
|
53
|
+
run: |
|
|
54
|
+
wheel="$(find dist -maxdepth 1 -type f -name '*.whl' -print -quit)"
|
|
55
|
+
if [[ -z "$wheel" ]]; then
|
|
56
|
+
echo "No wheel found in dist/."
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
uv venv --python 3.13 .smoke-venv
|
|
60
|
+
uv pip install --python .smoke-venv/bin/python "$wheel"
|
|
61
|
+
.smoke-venv/bin/python -c "import vocalbin; from importlib.metadata import version; print(version('vocalbin'))"
|
|
62
|
+
|
|
63
|
+
- name: Upload distributions
|
|
64
|
+
uses: actions/upload-artifact@v7
|
|
65
|
+
with:
|
|
66
|
+
name: python-package-distributions
|
|
67
|
+
path: dist/
|
|
68
|
+
if-no-files-found: error
|
|
69
|
+
retention-days: 7
|
|
70
|
+
|
|
71
|
+
publish-pypi:
|
|
72
|
+
needs: build
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
environment:
|
|
75
|
+
name: pypi
|
|
76
|
+
url: https://pypi.org/p/vocalbin
|
|
77
|
+
permissions:
|
|
78
|
+
id-token: write
|
|
79
|
+
steps:
|
|
80
|
+
- name: Download distributions
|
|
81
|
+
uses: actions/download-artifact@v8
|
|
82
|
+
with:
|
|
83
|
+
name: python-package-distributions
|
|
84
|
+
path: dist/
|
|
85
|
+
|
|
86
|
+
- name: Publish to PyPI
|
|
87
|
+
uses: pypa/gh-action-pypi-publish@v1.14.0
|
|
88
|
+
|
|
89
|
+
github-release:
|
|
90
|
+
needs: publish-pypi
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
permissions:
|
|
93
|
+
contents: write
|
|
94
|
+
steps:
|
|
95
|
+
- name: Download distributions
|
|
96
|
+
uses: actions/download-artifact@v8
|
|
97
|
+
with:
|
|
98
|
+
name: python-package-distributions
|
|
99
|
+
path: dist/
|
|
100
|
+
|
|
101
|
+
- name: Create GitHub release
|
|
102
|
+
env:
|
|
103
|
+
GH_TOKEN: ${{ github.token }}
|
|
104
|
+
run: >-
|
|
105
|
+
gh release create "$GITHUB_REF_NAME" dist/*
|
|
106
|
+
--repo "$GITHUB_REPOSITORY"
|
|
107
|
+
--verify-tag
|
|
108
|
+
--generate-notes
|
|
109
|
+
--title "$GITHUB_REF_NAME"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
unit-tests:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Check out repository
|
|
17
|
+
uses: actions/checkout@v7
|
|
18
|
+
|
|
19
|
+
- name: Install uv and Python
|
|
20
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
21
|
+
with:
|
|
22
|
+
version: "0.11.30"
|
|
23
|
+
python-version: "3.13"
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --locked --dev
|
|
28
|
+
|
|
29
|
+
- name: Run unit tests
|
|
30
|
+
run: uv run pytest
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
|
|
51
|
+
# Jupyter Notebook
|
|
52
|
+
.ipynb_checkpoints
|
|
53
|
+
|
|
54
|
+
# Environments
|
|
55
|
+
.env
|
|
56
|
+
.venv
|
|
57
|
+
env/
|
|
58
|
+
venv/
|
|
59
|
+
ENV/
|
|
60
|
+
env.bak/
|
|
61
|
+
venv.bak/
|
|
62
|
+
|
|
63
|
+
# mypy
|
|
64
|
+
.mypy_cache/
|
|
65
|
+
.dmypy.json
|
|
66
|
+
dmypy.json
|
|
67
|
+
|
|
68
|
+
# Pyre type checker
|
|
69
|
+
.pyre/
|
|
70
|
+
|
|
71
|
+
# pytype static type analyzer
|
|
72
|
+
.pytype/
|
|
73
|
+
|
|
74
|
+
# ruff
|
|
75
|
+
.ruff_cache/
|
|
76
|
+
|
|
77
|
+
# IDEs
|
|
78
|
+
.idea/
|
|
79
|
+
.vscode/
|
|
80
|
+
|
|
81
|
+
# Claude Code
|
|
82
|
+
.claude/
|
|
83
|
+
|
|
84
|
+
# OS
|
|
85
|
+
.DS_Store
|
|
86
|
+
Thumbs.db
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-toml
|
|
9
|
+
- id: check-added-large-files
|
|
10
|
+
- id: check-merge-conflict
|
|
11
|
+
- id: debug-statements
|
|
12
|
+
|
|
13
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
14
|
+
rev: v0.12.0
|
|
15
|
+
hooks:
|
|
16
|
+
- id: ruff
|
|
17
|
+
args: [--fix]
|
|
18
|
+
- id: ruff-format
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
vocalbin-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Repository conventions
|
|
2
|
+
|
|
3
|
+
- Use relative imports for package re-exports in `__init__.py`; use absolute imports in all other modules.
|
|
4
|
+
- Model provider interfaces with `ABC` and `@abstractmethod`, not `Protocol`.
|
|
5
|
+
- Keep provider ports in `vocalbin/ports.py`.
|
|
6
|
+
- Keep credential loading in `vocalbin/credentials.py` and use `pydantic-settings`.
|
|
7
|
+
- Avoid comments and docstrings that merely restate what the code already says.
|
|
8
|
+
- Add a comment only when it explains a non-obvious reason, constraint, or tradeoff.
|
|
9
|
+
- Do not add module docstrings that only summarize the module name or contents.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Contributing to vocalbin
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in improving `vocalbin`. This project is intentionally
|
|
4
|
+
small: a typed, asynchronous wrapper around OpenAI's speech-to-text and
|
|
5
|
+
text-to-speech endpoints. A small API surface, clear types, few dependencies and
|
|
6
|
+
predictable behavior matter more than a long feature list.
|
|
7
|
+
|
|
8
|
+
## Scope: read the Vision first
|
|
9
|
+
|
|
10
|
+
Before opening an issue or pull request, please read [`vision.md`](vision.md). It
|
|
11
|
+
defines what this library does, what it deliberately does **not** do, and the
|
|
12
|
+
guardrails for future changes.
|
|
13
|
+
|
|
14
|
+
A change is in scope only if you can answer **yes** to the five questions listed
|
|
15
|
+
under _"Leitlinie für zukünftige Pull Requests"_ in [`vision.md`](vision.md) — in
|
|
16
|
+
short:
|
|
17
|
+
|
|
18
|
+
1. Does it directly support speech-to-text or text-to-speech via the OpenAI Audio API?
|
|
19
|
+
2. Does it fit the existing request/response/port/client boundaries without adding
|
|
20
|
+
application-specific dependencies?
|
|
21
|
+
3. Does the public API stay small, async, typed and understandable?
|
|
22
|
+
4. Is the added dependency or complexity justified by a concrete benefit?
|
|
23
|
+
5. Are behavior, validation and backward compatibility covered by tests?
|
|
24
|
+
|
|
25
|
+
Work that needs audio processing, framework integration, persistence or
|
|
26
|
+
orchestration belongs in the calling application or a separate package, not here.
|
|
27
|
+
|
|
28
|
+
## Development setup
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv sync
|
|
32
|
+
uv run pytest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Tests run offline against fake clients, so no `OPENAI_API_KEY` is required for the
|
|
36
|
+
suite. A key is only needed to run the scripts in [`examples/`](examples/) against
|
|
37
|
+
the real API.
|
|
38
|
+
|
|
39
|
+
## Code conventions
|
|
40
|
+
|
|
41
|
+
These follow [`AGENTS.md`](AGENTS.md):
|
|
42
|
+
|
|
43
|
+
- Use relative imports for package re-exports in `__init__.py`; use absolute
|
|
44
|
+
imports everywhere else.
|
|
45
|
+
- Model provider interfaces with `ABC` and `@abstractmethod`, not `Protocol`.
|
|
46
|
+
Keep the provider ports in `vocalbin/ports.py`.
|
|
47
|
+
- Avoid comments and docstrings that merely restate the code. Add a comment only
|
|
48
|
+
when it explains a non-obvious reason, constraint or tradeoff.
|
|
49
|
+
- Validate model capabilities up front (see `vocalbin/models.py`) and keep raw
|
|
50
|
+
provider data available on responses rather than discarding it.
|
|
51
|
+
|
|
52
|
+
## Pull requests
|
|
53
|
+
|
|
54
|
+
- Keep changes focused; split unrelated work into separate pull requests.
|
|
55
|
+
- Add or update tests for any behavior, validation rule or capability change.
|
|
56
|
+
- Update the [`README.md`](README.md) and the relevant script(s) in
|
|
57
|
+
[`examples/`](examples/) when you add or change a model, voice, format or
|
|
58
|
+
parameter.
|
|
59
|
+
- Make sure `uv run pytest` passes before submitting.
|
|
60
|
+
|
|
61
|
+
A pull request that widens the project's scope only because the implementation is
|
|
62
|
+
technically possible here should be declined or split, as described in
|
|
63
|
+
[`vision.md`](vision.md).
|
vocalbin-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mathisarends
|
|
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.
|
vocalbin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vocalbin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed async wrappers for OpenAI speech-to-text and text-to-speech APIs
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Requires-Dist: openai>=2.46.0
|
|
8
|
+
Requires-Dist: pydantic-settings>=2.14.2
|
|
9
|
+
Requires-Dist: pydantic>=2.13.4
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# 🎙️ vocalbin
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
`vocalbin` is a small, typed, asynchronous wrapper around OpenAI's speech-to-text
|
|
17
|
+
and text-to-speech endpoints. It validates model capabilities up front, normalizes
|
|
18
|
+
responses without discarding raw data, and stays independent of any
|
|
19
|
+
application-specific settings or domain code.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv add vocalbin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Set `OPENAI_API_KEY` in the environment, or pass an API key directly when creating
|
|
28
|
+
a service. The default path reads the environment through `OpenAICredentials`:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from vocalbin import OpenAICredentials
|
|
32
|
+
|
|
33
|
+
credentials = OpenAICredentials()
|
|
34
|
+
api_key = credentials.api_key.get_secret_value()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
An explicit `api_key` takes precedence over the environment. An injected
|
|
38
|
+
`AsyncOpenAI` client does not load credentials at all.
|
|
39
|
+
|
|
40
|
+
## Speech to text
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
from vocalbin import OpenAISpeechToText, SpeechToTextRequest
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def transcribe() -> str:
|
|
49
|
+
async with OpenAISpeechToText() as speech_to_text:
|
|
50
|
+
response = await speech_to_text.transcribe(
|
|
51
|
+
SpeechToTextRequest(audio_path=Path("speech.wav"), language="de")
|
|
52
|
+
)
|
|
53
|
+
return response.text
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Audio can also be supplied directly as bytes; `filename` only sets the multipart
|
|
57
|
+
upload name:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
request = SpeechToTextRequest(audio=audio_bytes, filename="speech.wav")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Every request carries the transcript on `response.text` and the untouched provider
|
|
64
|
+
payload on `response.raw` (a `dict` for JSON-like formats, a `str` for `text`,
|
|
65
|
+
`srt` and `vtt`).
|
|
66
|
+
|
|
67
|
+
## Text to speech
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from vocalbin import (
|
|
71
|
+
OpenAITextToSpeech,
|
|
72
|
+
TextToSpeechFormat,
|
|
73
|
+
TextToSpeechRequest,
|
|
74
|
+
TextToSpeechVoice,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def synthesize() -> bytes:
|
|
79
|
+
async with OpenAITextToSpeech() as text_to_speech:
|
|
80
|
+
response = await text_to_speech.synthesize(
|
|
81
|
+
TextToSpeechRequest(
|
|
82
|
+
text="Hallo aus vocalbin!",
|
|
83
|
+
voice=TextToSpeechVoice.MARIN,
|
|
84
|
+
response_format=TextToSpeechFormat.MP3,
|
|
85
|
+
instructions="Sprich ruhig und freundlich.",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
return response.audio
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`response.content_type` gives the matching MIME type (e.g. `audio/mpeg`).
|
|
92
|
+
|
|
93
|
+
## Supported models, voices and formats
|
|
94
|
+
|
|
95
|
+
**Speech to text** — `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`,
|
|
96
|
+
`gpt-4o-transcribe-diarize`, `whisper-1`. Response formats and options are
|
|
97
|
+
validated per model (for example, `timestamp_granularities` require `whisper-1`
|
|
98
|
+
with `verbose_json`, and `include=["logprobs"]` requires a GPT transcription model
|
|
99
|
+
with `json`).
|
|
100
|
+
|
|
101
|
+
**Text to speech** — `gpt-4o-mini-tts`, `tts-1`, `tts-1-hd`; output formats `mp3`,
|
|
102
|
+
`opus`, `aac`, `flac`, `wav`, `pcm`. The legacy `tts-1`/`tts-1-hd` models accept
|
|
103
|
+
only the legacy voices and do not support `instructions`.
|
|
104
|
+
|
|
105
|
+
## Examples
|
|
106
|
+
|
|
107
|
+
The [`examples/`](examples/) directory holds runnable, integration-testable scripts
|
|
108
|
+
that exercise every model/voice/format combination and double as documentation.
|
|
109
|
+
With a valid `OPENAI_API_KEY` set:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uv run python examples/text_to_speech.py # every TTS model, voice and format
|
|
113
|
+
uv run python examples/speech_to_text.py # every STT model and response format
|
|
114
|
+
uv run python examples/round_trip.py # synthesize -> transcribe, self-checking
|
|
115
|
+
uv run python examples/shared_client.py # one AsyncOpenAI client for both services
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Generated audio and transcripts are written to `examples/output/` (git-ignored).
|
|
119
|
+
`speech_to_text.py` synthesizes its own `sample.wav` on first run, so it needs no
|
|
120
|
+
external audio file.
|
|
121
|
+
|
|
122
|
+
## Bring your own client
|
|
123
|
+
|
|
124
|
+
Both concrete services accept an existing `AsyncOpenAI` instance via `client=`,
|
|
125
|
+
which lets you share one configured client (custom `base_url`, timeouts, retries)
|
|
126
|
+
across both services. Injected clients remain owned by the caller and are not
|
|
127
|
+
closed by `vocalbin`:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from openai import AsyncOpenAI
|
|
131
|
+
|
|
132
|
+
from vocalbin import OpenAISpeechToText, OpenAITextToSpeech
|
|
133
|
+
|
|
134
|
+
client = AsyncOpenAI()
|
|
135
|
+
tts = OpenAITextToSpeech(client=client)
|
|
136
|
+
stt = OpenAISpeechToText(client=client)
|
|
137
|
+
# ... use both, then close it yourself:
|
|
138
|
+
await client.close()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Ports
|
|
142
|
+
|
|
143
|
+
The provider-independent `SpeechToText` and `TextToSpeech` ports are abstract base
|
|
144
|
+
classes (`vocalbin/ports.py`). They mark the boundary of the library, so callers
|
|
145
|
+
can depend on the interface rather than the OpenAI implementation.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
uv sync
|
|
151
|
+
uv run pytest
|
|
152
|
+
```
|
vocalbin-0.1.0/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# 🎙️ vocalbin
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
`vocalbin` is a small, typed, asynchronous wrapper around OpenAI's speech-to-text
|
|
6
|
+
and text-to-speech endpoints. It validates model capabilities up front, normalizes
|
|
7
|
+
responses without discarding raw data, and stays independent of any
|
|
8
|
+
application-specific settings or domain code.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv add vocalbin
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Set `OPENAI_API_KEY` in the environment, or pass an API key directly when creating
|
|
17
|
+
a service. The default path reads the environment through `OpenAICredentials`:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from vocalbin import OpenAICredentials
|
|
21
|
+
|
|
22
|
+
credentials = OpenAICredentials()
|
|
23
|
+
api_key = credentials.api_key.get_secret_value()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
An explicit `api_key` takes precedence over the environment. An injected
|
|
27
|
+
`AsyncOpenAI` client does not load credentials at all.
|
|
28
|
+
|
|
29
|
+
## Speech to text
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from vocalbin import OpenAISpeechToText, SpeechToTextRequest
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def transcribe() -> str:
|
|
38
|
+
async with OpenAISpeechToText() as speech_to_text:
|
|
39
|
+
response = await speech_to_text.transcribe(
|
|
40
|
+
SpeechToTextRequest(audio_path=Path("speech.wav"), language="de")
|
|
41
|
+
)
|
|
42
|
+
return response.text
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Audio can also be supplied directly as bytes; `filename` only sets the multipart
|
|
46
|
+
upload name:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
request = SpeechToTextRequest(audio=audio_bytes, filename="speech.wav")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every request carries the transcript on `response.text` and the untouched provider
|
|
53
|
+
payload on `response.raw` (a `dict` for JSON-like formats, a `str` for `text`,
|
|
54
|
+
`srt` and `vtt`).
|
|
55
|
+
|
|
56
|
+
## Text to speech
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from vocalbin import (
|
|
60
|
+
OpenAITextToSpeech,
|
|
61
|
+
TextToSpeechFormat,
|
|
62
|
+
TextToSpeechRequest,
|
|
63
|
+
TextToSpeechVoice,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def synthesize() -> bytes:
|
|
68
|
+
async with OpenAITextToSpeech() as text_to_speech:
|
|
69
|
+
response = await text_to_speech.synthesize(
|
|
70
|
+
TextToSpeechRequest(
|
|
71
|
+
text="Hallo aus vocalbin!",
|
|
72
|
+
voice=TextToSpeechVoice.MARIN,
|
|
73
|
+
response_format=TextToSpeechFormat.MP3,
|
|
74
|
+
instructions="Sprich ruhig und freundlich.",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
return response.audio
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`response.content_type` gives the matching MIME type (e.g. `audio/mpeg`).
|
|
81
|
+
|
|
82
|
+
## Supported models, voices and formats
|
|
83
|
+
|
|
84
|
+
**Speech to text** — `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`,
|
|
85
|
+
`gpt-4o-transcribe-diarize`, `whisper-1`. Response formats and options are
|
|
86
|
+
validated per model (for example, `timestamp_granularities` require `whisper-1`
|
|
87
|
+
with `verbose_json`, and `include=["logprobs"]` requires a GPT transcription model
|
|
88
|
+
with `json`).
|
|
89
|
+
|
|
90
|
+
**Text to speech** — `gpt-4o-mini-tts`, `tts-1`, `tts-1-hd`; output formats `mp3`,
|
|
91
|
+
`opus`, `aac`, `flac`, `wav`, `pcm`. The legacy `tts-1`/`tts-1-hd` models accept
|
|
92
|
+
only the legacy voices and do not support `instructions`.
|
|
93
|
+
|
|
94
|
+
## Examples
|
|
95
|
+
|
|
96
|
+
The [`examples/`](examples/) directory holds runnable, integration-testable scripts
|
|
97
|
+
that exercise every model/voice/format combination and double as documentation.
|
|
98
|
+
With a valid `OPENAI_API_KEY` set:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
uv run python examples/text_to_speech.py # every TTS model, voice and format
|
|
102
|
+
uv run python examples/speech_to_text.py # every STT model and response format
|
|
103
|
+
uv run python examples/round_trip.py # synthesize -> transcribe, self-checking
|
|
104
|
+
uv run python examples/shared_client.py # one AsyncOpenAI client for both services
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Generated audio and transcripts are written to `examples/output/` (git-ignored).
|
|
108
|
+
`speech_to_text.py` synthesizes its own `sample.wav` on first run, so it needs no
|
|
109
|
+
external audio file.
|
|
110
|
+
|
|
111
|
+
## Bring your own client
|
|
112
|
+
|
|
113
|
+
Both concrete services accept an existing `AsyncOpenAI` instance via `client=`,
|
|
114
|
+
which lets you share one configured client (custom `base_url`, timeouts, retries)
|
|
115
|
+
across both services. Injected clients remain owned by the caller and are not
|
|
116
|
+
closed by `vocalbin`:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from openai import AsyncOpenAI
|
|
120
|
+
|
|
121
|
+
from vocalbin import OpenAISpeechToText, OpenAITextToSpeech
|
|
122
|
+
|
|
123
|
+
client = AsyncOpenAI()
|
|
124
|
+
tts = OpenAITextToSpeech(client=client)
|
|
125
|
+
stt = OpenAISpeechToText(client=client)
|
|
126
|
+
# ... use both, then close it yourself:
|
|
127
|
+
await client.close()
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Ports
|
|
131
|
+
|
|
132
|
+
The provider-independent `SpeechToText` and `TextToSpeech` ports are abstract base
|
|
133
|
+
classes (`vocalbin/ports.py`). They mark the boundary of the library, so callers
|
|
134
|
+
can depend on the interface rather than the OpenAI implementation.
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
uv sync
|
|
140
|
+
uv run pytest
|
|
141
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Deployment
|
|
2
|
+
|
|
3
|
+
`.github/workflows/release.yml` publishes `vocalbin` to PyPI when a `v*` tag is
|
|
4
|
+
pushed. Authentication uses PyPI Trusted Publishing (OIDC); no API token is
|
|
5
|
+
stored in GitHub.
|
|
6
|
+
|
|
7
|
+
## One-time setup
|
|
8
|
+
|
|
9
|
+
1. Create a GitHub environment named `pypi` under **Settings > Environments**.
|
|
10
|
+
Optionally require approval and restrict deployments to tags matching `v*`.
|
|
11
|
+
2. Add a GitHub Trusted Publisher to the `vocalbin` project on PyPI:
|
|
12
|
+
|
|
13
|
+
| Setting | Value |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| Owner | `mathisarends` |
|
|
16
|
+
| Repository | `vocalbin` |
|
|
17
|
+
| Workflow | `release.yml` |
|
|
18
|
+
| Environment | `pypi` |
|
|
19
|
+
|
|
20
|
+
If the PyPI project does not exist yet, create a pending publisher at
|
|
21
|
+
<https://pypi.org/manage/account/publishing/> using the same values.
|
|
22
|
+
|
|
23
|
+
## Release
|
|
24
|
+
|
|
25
|
+
Set and commit the release version:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv version 0.2.0
|
|
29
|
+
uv run pytest
|
|
30
|
+
git add pyproject.toml uv.lock
|
|
31
|
+
git commit -m "Release 0.2.0"
|
|
32
|
+
git push origin main
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
After the `Tests` workflow succeeds on `main`, create the matching tag:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
version="$(uv version --short)"
|
|
39
|
+
git tag -a "v$version" -m "Release v$version"
|
|
40
|
+
git push origin "v$version"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The release workflow then:
|
|
44
|
+
|
|
45
|
+
1. Confirms that the tagged commit belongs to `main` and the tag matches the
|
|
46
|
+
version in `pyproject.toml`.
|
|
47
|
+
2. Installs the locked dependencies and runs all tests with 100% statement and
|
|
48
|
+
branch coverage.
|
|
49
|
+
3. Builds the wheel and source distribution, checks their metadata, and installs
|
|
50
|
+
the wheel in a clean environment.
|
|
51
|
+
4. Publishes the verified artifacts to PyPI through OIDC.
|
|
52
|
+
5. Creates a GitHub Release with generated notes and both distributions attached.
|
|
53
|
+
|
|
54
|
+
## Failures
|
|
55
|
+
|
|
56
|
+
- PyPI versions are immutable. Fix a broken release with a new version; yank the
|
|
57
|
+
old version if necessary.
|
|
58
|
+
- An OIDC error usually means the owner, repository, workflow, or environment
|
|
59
|
+
differs from the PyPI Trusted Publisher configuration.
|
|
60
|
+
- A mismatched tag is rejected before publishing. Create a tag matching
|
|
61
|
+
`uv version --short`.
|