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.
Files changed (48) hide show
  1. voiceblender-0.12.2/.claude/settings.json +8 -0
  2. voiceblender-0.12.2/.claude/settings.local.json +14 -0
  3. voiceblender-0.12.2/.gitignore +17 -0
  4. voiceblender-0.12.2/LICENSE +21 -0
  5. voiceblender-0.12.2/Makefile +85 -0
  6. voiceblender-0.12.2/PKG-INFO +123 -0
  7. voiceblender-0.12.2/README.md +70 -0
  8. voiceblender-0.12.2/examples/ivr/.env.example +21 -0
  9. voiceblender-0.12.2/examples/ivr/README.md +118 -0
  10. voiceblender-0.12.2/examples/ivr/main.py +676 -0
  11. voiceblender-0.12.2/pyproject.toml +126 -0
  12. voiceblender-0.12.2/src/voiceblender/__init__.py +323 -0
  13. voiceblender-0.12.2/src/voiceblender/_client.py +212 -0
  14. voiceblender-0.12.2/src/voiceblender/_errors.py +91 -0
  15. voiceblender-0.12.2/src/voiceblender/_events.py +1101 -0
  16. voiceblender-0.12.2/src/voiceblender/_http.py +97 -0
  17. voiceblender-0.12.2/src/voiceblender/_hub.py +270 -0
  18. voiceblender-0.12.2/src/voiceblender/_legs.py +856 -0
  19. voiceblender-0.12.2/src/voiceblender/_models.py +165 -0
  20. voiceblender-0.12.2/src/voiceblender/_playback.py +65 -0
  21. voiceblender-0.12.2/src/voiceblender/_requests.py +952 -0
  22. voiceblender-0.12.2/src/voiceblender/_responses.py +18 -0
  23. voiceblender-0.12.2/src/voiceblender/_responses_extra.py +86 -0
  24. voiceblender-0.12.2/src/voiceblender/_rooms.py +408 -0
  25. voiceblender-0.12.2/src/voiceblender/_stream.py +292 -0
  26. voiceblender-0.12.2/src/voiceblender/_sync_helpers.py +354 -0
  27. voiceblender-0.12.2/src/voiceblender/_version.py +24 -0
  28. voiceblender-0.12.2/src/voiceblender/_vsi.py +1650 -0
  29. voiceblender-0.12.2/src/voiceblender/_webrtc.py +55 -0
  30. voiceblender-0.12.2/src/voiceblender/py.typed +0 -0
  31. voiceblender-0.12.2/src/voiceblender/sync/__init__.py +22 -0
  32. voiceblender-0.12.2/src/voiceblender/sync/_facade.py +284 -0
  33. voiceblender-0.12.2/tests/__init__.py +0 -0
  34. voiceblender-0.12.2/tests/_vsi_mock.py +104 -0
  35. voiceblender-0.12.2/tests/conftest.py +25 -0
  36. voiceblender-0.12.2/tests/test_errors.py +54 -0
  37. voiceblender-0.12.2/tests/test_generator_determinism.py +107 -0
  38. voiceblender-0.12.2/tests/test_http_flow.py +177 -0
  39. voiceblender-0.12.2/tests/test_hub.py +124 -0
  40. voiceblender-0.12.2/tests/test_import.py +26 -0
  41. voiceblender-0.12.2/tests/test_ivr_example.py +348 -0
  42. voiceblender-0.12.2/tests/test_naming.py +72 -0
  43. voiceblender-0.12.2/tests/test_parity.py +177 -0
  44. voiceblender-0.12.2/tests/test_playback.py +45 -0
  45. voiceblender-0.12.2/tests/test_roundtrip.py +182 -0
  46. voiceblender-0.12.2/tests/test_sync_facade.py +138 -0
  47. voiceblender-0.12.2/tests/test_vsi_stream.py +126 -0
  48. voiceblender-0.12.2/tools/generate.py +1931 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//home/csiwek/go/src/voiceblender-workspace/**)",
5
+ "Bash(ls -la voiceblender-go/)"
6
+ ]
7
+ }
8
+ }
@@ -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.