voiceblender 0.12.2__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.
- voiceblender-0.12.2/.claude/settings.json +8 -0
- voiceblender-0.12.2/.claude/settings.local.json +14 -0
- voiceblender-0.12.2/.gitignore +17 -0
- voiceblender-0.12.2/LICENSE +21 -0
- voiceblender-0.12.2/Makefile +85 -0
- voiceblender-0.12.2/PKG-INFO +123 -0
- voiceblender-0.12.2/README.md +70 -0
- voiceblender-0.12.2/examples/ivr/.env.example +21 -0
- voiceblender-0.12.2/examples/ivr/README.md +118 -0
- voiceblender-0.12.2/examples/ivr/main.py +676 -0
- voiceblender-0.12.2/pyproject.toml +126 -0
- voiceblender-0.12.2/src/voiceblender/__init__.py +323 -0
- voiceblender-0.12.2/src/voiceblender/_client.py +212 -0
- voiceblender-0.12.2/src/voiceblender/_errors.py +91 -0
- voiceblender-0.12.2/src/voiceblender/_events.py +1101 -0
- voiceblender-0.12.2/src/voiceblender/_http.py +97 -0
- voiceblender-0.12.2/src/voiceblender/_hub.py +270 -0
- voiceblender-0.12.2/src/voiceblender/_legs.py +856 -0
- voiceblender-0.12.2/src/voiceblender/_models.py +165 -0
- voiceblender-0.12.2/src/voiceblender/_playback.py +65 -0
- voiceblender-0.12.2/src/voiceblender/_requests.py +952 -0
- voiceblender-0.12.2/src/voiceblender/_responses.py +18 -0
- voiceblender-0.12.2/src/voiceblender/_responses_extra.py +86 -0
- voiceblender-0.12.2/src/voiceblender/_rooms.py +408 -0
- voiceblender-0.12.2/src/voiceblender/_stream.py +292 -0
- voiceblender-0.12.2/src/voiceblender/_sync_helpers.py +354 -0
- voiceblender-0.12.2/src/voiceblender/_version.py +24 -0
- voiceblender-0.12.2/src/voiceblender/_vsi.py +1650 -0
- voiceblender-0.12.2/src/voiceblender/_webrtc.py +55 -0
- voiceblender-0.12.2/src/voiceblender/py.typed +0 -0
- voiceblender-0.12.2/src/voiceblender/sync/__init__.py +22 -0
- voiceblender-0.12.2/src/voiceblender/sync/_facade.py +284 -0
- voiceblender-0.12.2/tests/__init__.py +0 -0
- voiceblender-0.12.2/tests/_vsi_mock.py +104 -0
- voiceblender-0.12.2/tests/conftest.py +25 -0
- voiceblender-0.12.2/tests/test_errors.py +54 -0
- voiceblender-0.12.2/tests/test_generator_determinism.py +107 -0
- voiceblender-0.12.2/tests/test_http_flow.py +177 -0
- voiceblender-0.12.2/tests/test_hub.py +124 -0
- voiceblender-0.12.2/tests/test_import.py +26 -0
- voiceblender-0.12.2/tests/test_ivr_example.py +348 -0
- voiceblender-0.12.2/tests/test_naming.py +72 -0
- voiceblender-0.12.2/tests/test_parity.py +177 -0
- voiceblender-0.12.2/tests/test_playback.py +45 -0
- voiceblender-0.12.2/tests/test_roundtrip.py +182 -0
- voiceblender-0.12.2/tests/test_sync_facade.py +138 -0
- voiceblender-0.12.2/tests/test_vsi_stream.py +126 -0
- voiceblender-0.12.2/tools/generate.py +1931 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(make generate *)",
|
|
5
|
+
"Bash(python3 -c \"import sys; print\\(sys.prefix\\)\")",
|
|
6
|
+
"Bash(python3 -m pip list)",
|
|
7
|
+
"Bash(python3 -m pip show ruamel.yaml)",
|
|
8
|
+
"Bash(.venv/bin/python -c \"import ruamel.yaml, sys; print\\('ruamel OK in .venv:', ruamel.yaml.__version__ if hasattr\\(ruamel.yaml,'__version__'\\) else 'installed'\\); print\\(sys.executable\\)\")",
|
|
9
|
+
"Bash(.venv/bin/python -m pip list)",
|
|
10
|
+
"Bash(git checkout *)",
|
|
11
|
+
"Bash(PATH=__TRACKED_VAR__/.venv/bin:__TRACKED_VAR__ PY=.venv/bin/python make generate)"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
.venv/
|
|
5
|
+
venv/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.coverage
|
|
13
|
+
htmlcov/
|
|
14
|
+
.tox/
|
|
15
|
+
|
|
16
|
+
# Written at build time by hatch-vcs from the git tag.
|
|
17
|
+
src/voiceblender/_version.py
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Messageroute Ltd
|
|
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,85 @@
|
|
|
1
|
+
# VOICEBLENDER points to the VoiceBlender repository root.
|
|
2
|
+
# Override on the command line: make generate VOICEBLENDER=/other/path
|
|
3
|
+
VOICEBLENDER ?= ../VoiceBlender
|
|
4
|
+
OPENAPI := $(VOICEBLENDER)/openapi.yaml
|
|
5
|
+
ASYNCAPI := $(VOICEBLENDER)/asyncapi.yaml
|
|
6
|
+
|
|
7
|
+
PY ?= python3
|
|
8
|
+
|
|
9
|
+
.PHONY: generate lint format typecheck test build package release-check publish publish-test install-dev clean
|
|
10
|
+
|
|
11
|
+
# generate reads openapi.yaml + asyncapi.yaml and rewrites the generated files
|
|
12
|
+
# (_models.py, _requests.py, _responses.py, _events.py, _legs.py, _rooms.py,
|
|
13
|
+
# _webrtc.py, _vsi.py). Run this whenever either spec changes.
|
|
14
|
+
generate:
|
|
15
|
+
$(PY) tools/generate.py --openapi $(OPENAPI) --asyncapi $(ASYNCAPI) --out src/voiceblender
|
|
16
|
+
ruff format src/voiceblender
|
|
17
|
+
ruff check --fix src/voiceblender
|
|
18
|
+
$(MAKE) typecheck
|
|
19
|
+
|
|
20
|
+
lint:
|
|
21
|
+
ruff check src/voiceblender tools tests
|
|
22
|
+
|
|
23
|
+
format:
|
|
24
|
+
ruff format src/voiceblender tools tests
|
|
25
|
+
|
|
26
|
+
typecheck:
|
|
27
|
+
mypy src/voiceblender
|
|
28
|
+
|
|
29
|
+
test:
|
|
30
|
+
pytest -q
|
|
31
|
+
|
|
32
|
+
build:
|
|
33
|
+
$(PY) -m build
|
|
34
|
+
|
|
35
|
+
# package runs the full check suite, empties dist/ and builds a fresh
|
|
36
|
+
# sdist + wheel, then validates the metadata twine will upload. Emptying dist/
|
|
37
|
+
# matters: `twine upload dist/*` would otherwise also push artifacts left over
|
|
38
|
+
# from an earlier version.
|
|
39
|
+
#
|
|
40
|
+
# The version is not in pyproject.toml — hatch-vcs derives it from the git tag,
|
|
41
|
+
# so `git tag -a v1.2.3 -m ...` is the whole version bump.
|
|
42
|
+
package: lint typecheck test
|
|
43
|
+
rm -rf dist
|
|
44
|
+
$(PY) -m build
|
|
45
|
+
$(PY) -m twine check dist/*
|
|
46
|
+
@echo "built version $$(ls dist/*.tar.gz | sed 's|.*/voiceblender-||; s|\.tar\.gz$$||')"
|
|
47
|
+
|
|
48
|
+
# release-check refuses to upload anything that isn't a clean tagged release.
|
|
49
|
+
# An untagged or dirty tree builds a version like 0.12.2.dev0+ge a002ce; PyPI
|
|
50
|
+
# rejects local version segments outright, so catch it here with a message that
|
|
51
|
+
# says what to do instead of an opaque 400 after the upload.
|
|
52
|
+
release-check:
|
|
53
|
+
@v=$$(ls dist/*.tar.gz | sed 's|.*/voiceblender-||; s|\.tar\.gz$$||'); \
|
|
54
|
+
case "$$v" in \
|
|
55
|
+
*dev*|*+*) \
|
|
56
|
+
echo "refusing to publish $$v — not a release build."; \
|
|
57
|
+
echo "commit your changes, then tag the release: git tag -a vX.Y.Z -m 'vX.Y.Z'"; \
|
|
58
|
+
exit 1;; \
|
|
59
|
+
esac; \
|
|
60
|
+
echo "publishing $$v"
|
|
61
|
+
|
|
62
|
+
# publish uploads the freshly built dist/ to PyPI.
|
|
63
|
+
#
|
|
64
|
+
# The version is the git tag: tag `vX.Y.Z`, then run this. PyPI accepts a given
|
|
65
|
+
# version exactly once, and deleting a release does not free the number for
|
|
66
|
+
# re-upload, so a tag is effectively immutable once published.
|
|
67
|
+
# Credentials come from ~/.pypirc, or from the environment:
|
|
68
|
+
#
|
|
69
|
+
# TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-<api-token> make publish
|
|
70
|
+
#
|
|
71
|
+
# Use `make publish-test` for a dry run against TestPyPI first.
|
|
72
|
+
publish: package release-check
|
|
73
|
+
$(PY) -m twine upload dist/*
|
|
74
|
+
|
|
75
|
+
# publish-test uploads to TestPyPI (https://test.pypi.org) instead, which needs
|
|
76
|
+
# its own account and API token under a [testpypi] section in ~/.pypirc.
|
|
77
|
+
publish-test: package release-check
|
|
78
|
+
$(PY) -m twine upload --repository testpypi dist/*
|
|
79
|
+
|
|
80
|
+
install-dev:
|
|
81
|
+
$(PY) -m pip install -e ".[dev]"
|
|
82
|
+
|
|
83
|
+
clean:
|
|
84
|
+
rm -rf build/ dist/ *.egg-info src/voiceblender/__pycache__ tests/__pycache__
|
|
85
|
+
rm -rf .mypy_cache .pytest_cache .ruff_cache
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voiceblender
|
|
3
|
+
Version: 0.12.2
|
|
4
|
+
Summary: Python client for the VoiceBlender API
|
|
5
|
+
Project-URL: Homepage, https://voiceblender.com
|
|
6
|
+
Project-URL: Repository, https://github.com/VoiceBlender/voiceblender-python
|
|
7
|
+
Author: Messageroute Ltd
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Messageroute Ltd
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: sip,telephony,voice,voiceblender,webrtc
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Topic :: Communications :: Telephony
|
|
39
|
+
Classifier: Typing :: Typed
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
|
+
Requires-Dist: httpx>=0.27
|
|
42
|
+
Requires-Dist: pydantic>=2.6
|
|
43
|
+
Requires-Dist: websockets>=13
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
46
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
47
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
48
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
49
|
+
Requires-Dist: ruamel-yaml>=0.18; extra == 'dev'
|
|
50
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
51
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# voiceblender-python
|
|
55
|
+
|
|
56
|
+
> [!IMPORTANT]
|
|
57
|
+
> **This library is auto-generated.** Models, request/response/event types, and
|
|
58
|
+
> method bindings are produced by `tools/generate.py` from the OpenAPI/AsyncAPI
|
|
59
|
+
> specs (see [Code generation](#code-generation)). Do **not** edit the generated
|
|
60
|
+
> sources directly — your changes will be overwritten on the next run. Open pull
|
|
61
|
+
> requests against the generator (`tools/generate.py`) and/or the upstream specs
|
|
62
|
+
> only.
|
|
63
|
+
|
|
64
|
+
Python client for the [VoiceBlender](https://voiceblender.com) API.
|
|
65
|
+
|
|
66
|
+
VoiceBlender bridges SIP and WebRTC voice calls with multi-party audio mixing,
|
|
67
|
+
real-time speech-to-text, text-to-speech, AI agent integration, recording, and
|
|
68
|
+
webhook-based event delivery.
|
|
69
|
+
|
|
70
|
+
This is a Python port of [voiceblender-go](https://github.com/VoiceBlender/voiceblender-go).
|
|
71
|
+
Models, request types, response types, event types, and method bindings are
|
|
72
|
+
**code-generated** from `../VoiceBlender/openapi.yaml` + `asyncapi.yaml` by
|
|
73
|
+
`tools/generate.py` — the same spec-driven pattern as the Go SDK.
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install voiceblender
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Quick start (async)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
import voiceblender
|
|
86
|
+
|
|
87
|
+
async def main():
|
|
88
|
+
async with voiceblender.Client(base_url="http://localhost:8080/v1") as c:
|
|
89
|
+
leg = await c.create_leg(voiceblender.CreateLegRequest(
|
|
90
|
+
type=voiceblender.LegType.SIP_OUTBOUND,
|
|
91
|
+
to="sip:alice@example.com",
|
|
92
|
+
))
|
|
93
|
+
print("Created leg:", leg.id)
|
|
94
|
+
|
|
95
|
+
asyncio.run(main())
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Quick start (sync)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from voiceblender.sync import SyncClient
|
|
102
|
+
import voiceblender
|
|
103
|
+
|
|
104
|
+
with SyncClient(base_url="http://localhost:8080/v1") as c:
|
|
105
|
+
leg = c.create_leg(voiceblender.CreateLegRequest(
|
|
106
|
+
type=voiceblender.LegType.SIP_OUTBOUND,
|
|
107
|
+
to="sip:alice@example.com",
|
|
108
|
+
))
|
|
109
|
+
print("Created leg:", leg.id)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Code generation
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
make generate # regenerate from openapi.yaml + asyncapi.yaml
|
|
116
|
+
make test # run pytest
|
|
117
|
+
make lint # ruff check
|
|
118
|
+
make typecheck # mypy --strict
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# voiceblender-python
|
|
2
|
+
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> **This library is auto-generated.** Models, request/response/event types, and
|
|
5
|
+
> method bindings are produced by `tools/generate.py` from the OpenAPI/AsyncAPI
|
|
6
|
+
> specs (see [Code generation](#code-generation)). Do **not** edit the generated
|
|
7
|
+
> sources directly — your changes will be overwritten on the next run. Open pull
|
|
8
|
+
> requests against the generator (`tools/generate.py`) and/or the upstream specs
|
|
9
|
+
> only.
|
|
10
|
+
|
|
11
|
+
Python client for the [VoiceBlender](https://voiceblender.com) API.
|
|
12
|
+
|
|
13
|
+
VoiceBlender bridges SIP and WebRTC voice calls with multi-party audio mixing,
|
|
14
|
+
real-time speech-to-text, text-to-speech, AI agent integration, recording, and
|
|
15
|
+
webhook-based event delivery.
|
|
16
|
+
|
|
17
|
+
This is a Python port of [voiceblender-go](https://github.com/VoiceBlender/voiceblender-go).
|
|
18
|
+
Models, request types, response types, event types, and method bindings are
|
|
19
|
+
**code-generated** from `../VoiceBlender/openapi.yaml` + `asyncapi.yaml` by
|
|
20
|
+
`tools/generate.py` — the same spec-driven pattern as the Go SDK.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install voiceblender
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start (async)
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import asyncio
|
|
32
|
+
import voiceblender
|
|
33
|
+
|
|
34
|
+
async def main():
|
|
35
|
+
async with voiceblender.Client(base_url="http://localhost:8080/v1") as c:
|
|
36
|
+
leg = await c.create_leg(voiceblender.CreateLegRequest(
|
|
37
|
+
type=voiceblender.LegType.SIP_OUTBOUND,
|
|
38
|
+
to="sip:alice@example.com",
|
|
39
|
+
))
|
|
40
|
+
print("Created leg:", leg.id)
|
|
41
|
+
|
|
42
|
+
asyncio.run(main())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start (sync)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from voiceblender.sync import SyncClient
|
|
49
|
+
import voiceblender
|
|
50
|
+
|
|
51
|
+
with SyncClient(base_url="http://localhost:8080/v1") as c:
|
|
52
|
+
leg = c.create_leg(voiceblender.CreateLegRequest(
|
|
53
|
+
type=voiceblender.LegType.SIP_OUTBOUND,
|
|
54
|
+
to="sip:alice@example.com",
|
|
55
|
+
))
|
|
56
|
+
print("Created leg:", leg.id)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Code generation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
make generate # regenerate from openapi.yaml + asyncapi.yaml
|
|
63
|
+
make test # run pytest
|
|
64
|
+
make lint # ruff check
|
|
65
|
+
make typecheck # mypy --strict
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# VoiceBlender IVR — example environment variables
|
|
2
|
+
# Copy to .env and fill in the required values.
|
|
3
|
+
|
|
4
|
+
# VoiceBlender API base URL. The IVR connects to <VOICEBLENDER_URL>/vsi
|
|
5
|
+
# (with http:// → ws:// and https:// → wss:// substitution) over a single
|
|
6
|
+
# outbound WebSocket — no inbound port to open.
|
|
7
|
+
VOICEBLENDER_URL=http://localhost:8080/v1
|
|
8
|
+
|
|
9
|
+
# TTS API key (e.g. ElevenLabs).
|
|
10
|
+
# Optional if already configured in the VoiceBlender service.
|
|
11
|
+
# TTS_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
12
|
+
|
|
13
|
+
# Deepgram API key for the AI agent (operator queue, digit 0).
|
|
14
|
+
# DEEPGRAM_API_KEY=
|
|
15
|
+
|
|
16
|
+
# TTS voice / provider used for all IVR prompts.
|
|
17
|
+
TTS_VOICE=Rachel
|
|
18
|
+
TTS_PROVIDER=elevenlabs
|
|
19
|
+
|
|
20
|
+
# Company name spoken in the greeting.
|
|
21
|
+
COMPANY_NAME=Acme Corp
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Example: Company IVR (VSI / Python)
|
|
2
|
+
|
|
3
|
+
A multi-department IVR (Interactive Voice Response) that answers inbound calls,
|
|
4
|
+
greets the caller with a TTS prompt, presents a DTMF menu, and routes the
|
|
5
|
+
caller to a department room — wired up over a **single outbound WebSocket** to
|
|
6
|
+
VoiceBlender's `/v1/vsi` endpoint.
|
|
7
|
+
|
|
8
|
+
This is a VSI redesign of [`../../../voiceblender-go/examples/ivr`](../../../voiceblender-go/examples/ivr),
|
|
9
|
+
which uses REST + HTTP webhooks. The behaviour is identical; the I/O substrate
|
|
10
|
+
is different.
|
|
11
|
+
|
|
12
|
+
## Call flow
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Inbound call
|
|
16
|
+
└─ leg.ringing → early media: UK ringback for 3 s → answer
|
|
17
|
+
└─ leg.connected → "Thank you for calling Acme Corp. Please hold…"
|
|
18
|
+
└─ tts.finished → main menu prompt
|
|
19
|
+
└─ dtmf.received
|
|
20
|
+
1 → Sales queue
|
|
21
|
+
2 → Support queue
|
|
22
|
+
3 → Billing queue
|
|
23
|
+
0 → Operator queue (Deepgram AI agent)
|
|
24
|
+
9 → Repeat menu
|
|
25
|
+
* → Goodbye → hang up
|
|
26
|
+
? → "Invalid option, please try again" (max 3 attempts, then goodbye)
|
|
27
|
+
└─ leg.disconnected → cleanup
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Once a caller is routed, they are added to the department's persistent room
|
|
31
|
+
where agents can join to handle the call. Hold music is played in the room
|
|
32
|
+
while they wait.
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
SIP carrier
|
|
38
|
+
│ inbound INVITE
|
|
39
|
+
▼
|
|
40
|
+
VoiceBlender ◄──── outbound WS ◄──── IVR (this program)
|
|
41
|
+
│ events (leg.ringing, dtmf.received, tts.finished, …)
|
|
42
|
+
│
|
|
43
|
+
▼ ──── VSI command frames ──►
|
|
44
|
+
(same WebSocket, bidirectional)
|
|
45
|
+
|
|
46
|
+
◄──── <cmd>.result ────
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
No inbound HTTP server. No public DNS. No ngrok. The IVR is a plain WebSocket
|
|
50
|
+
client — deployable behind any firewall that allows outbound connections to
|
|
51
|
+
VoiceBlender.
|
|
52
|
+
|
|
53
|
+
## Prerequisites
|
|
54
|
+
|
|
55
|
+
- A running [VoiceBlender](https://github.com/VoiceBlender/voiceblender) instance
|
|
56
|
+
reachable on the network from this host
|
|
57
|
+
- An [ElevenLabs](https://elevenlabs.io) API key for TTS prompts, unless already
|
|
58
|
+
configured in VoiceBlender
|
|
59
|
+
- Python 3.10+
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
| Environment variable | Required | Default | Description |
|
|
64
|
+
|----------------------|----------|---------|-------------|
|
|
65
|
+
| `VOICEBLENDER_URL` | no | `http://localhost:8080/v1` | VoiceBlender API base URL |
|
|
66
|
+
| `TTS_API_KEY` | no | — | TTS API key (omit if pre-configured in VoiceBlender) |
|
|
67
|
+
| `TTS_VOICE` | no | `Rachel` | TTS voice name |
|
|
68
|
+
| `TTS_PROVIDER` | no | `elevenlabs` | TTS provider name |
|
|
69
|
+
| `DEEPGRAM_API_KEY` | no | — | Deepgram API key for the operator AI agent |
|
|
70
|
+
| `COMPANY_NAME` | no | `Acme Corp` | Company name spoken in greeting |
|
|
71
|
+
|
|
72
|
+
See [`.env.example`](.env.example) for a copy-paste-ready template.
|
|
73
|
+
|
|
74
|
+
## Running
|
|
75
|
+
|
|
76
|
+
From the `voiceblender-python` repo root:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install -e ".[dev]" # one-time
|
|
80
|
+
python examples/ivr/main.py
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
That's it. No port to forward, no tunnel to set up.
|
|
84
|
+
|
|
85
|
+
VoiceBlender must be configured to send inbound SIP calls to the same
|
|
86
|
+
instance. The IVR creates the four department rooms (`sales`, `support`,
|
|
87
|
+
`billing`, `operator`) on startup if they don't already exist — using the VSI
|
|
88
|
+
`create_room` command, no webhook registration required.
|
|
89
|
+
|
|
90
|
+
## Code structure
|
|
91
|
+
|
|
92
|
+
Each active call is a `Call` dataclass holding the current IVR state
|
|
93
|
+
(`GREETING → MENU → ROUTED/GOODBYE`). Events arrive over the WebSocket and
|
|
94
|
+
are dispatched to `asyncio.create_task` handlers so the read loop never
|
|
95
|
+
blocks; all state transitions are protected by a per-call `asyncio.Lock`.
|
|
96
|
+
|
|
97
|
+
| Old (REST + webhook) | New (VSI) |
|
|
98
|
+
|----------------------|-----------|
|
|
99
|
+
| `aiohttp.web.Application` + `/webhook` route | `client.events_stream()` + `client.subscribe()` |
|
|
100
|
+
| `voiceblender.Client._do(...)` (HTTP) | `stream.<vsi_method>(...)` (WS) |
|
|
101
|
+
| `leg.early_media(...)` / `leg.play(...)` / … | `stream.leg_early_media(payload)` / `stream.leg_play_start(payload)` / … |
|
|
102
|
+
| `room.add_leg(...)` / `room.play(...)` | `stream.add_leg_to_room(payload)` / `stream.room_play_start(payload)` |
|
|
103
|
+
| `client.create_room(...)` | `stream.create_room(CreateRoomRequest(...))` |
|
|
104
|
+
|
|
105
|
+
The TTS sequencing trick is identical: each call tracks its `active_tts_id`
|
|
106
|
+
and `tts.finished` events for replaced prompts are silently discarded.
|
|
107
|
+
|
|
108
|
+
## Reconnect
|
|
109
|
+
|
|
110
|
+
`main.py` wraps the stream loop in a simple `while True: try / except +
|
|
111
|
+
asyncio.sleep(5)` block. On any disconnect (network blip, server restart) the
|
|
112
|
+
IVR drops its in-memory call state and reconnects after a fixed 5-second
|
|
113
|
+
delay. In-flight calls that survive the disconnect would have to re-ring; that
|
|
114
|
+
matches what the server already does on its side.
|
|
115
|
+
|
|
116
|
+
A production deployment should swap the fixed delay for exponential backoff
|
|
117
|
+
and may want to reconcile per-call state across reconnects via `list_legs` /
|
|
118
|
+
`list_rooms` VSI commands.
|