vox-rtc-server 0.1.1__py3-none-any.whl
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.
- vox_rtc_server/__init__.py +30 -0
- vox_rtc_server/client.py +192 -0
- vox_rtc_server/py.typed +1 -0
- vox_rtc_server/session.py +156 -0
- vox_rtc_server/types.py +116 -0
- vox_rtc_server-0.1.1.dist-info/METADATA +79 -0
- vox_rtc_server-0.1.1.dist-info/RECORD +8 -0
- vox_rtc_server-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .client import VoxRtcServerClient
|
|
4
|
+
from .session import VoxRtcControlSession
|
|
5
|
+
from .types import (
|
|
6
|
+
ChannelState,
|
|
7
|
+
ClientEventEnvelope,
|
|
8
|
+
ConnectionState,
|
|
9
|
+
RTCIceServer,
|
|
10
|
+
ResponseOptions,
|
|
11
|
+
SessionBootstrap,
|
|
12
|
+
SessionConfig,
|
|
13
|
+
WireEvent,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.1"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ChannelState",
|
|
20
|
+
"ClientEventEnvelope",
|
|
21
|
+
"ConnectionState",
|
|
22
|
+
"RTCIceServer",
|
|
23
|
+
"ResponseOptions",
|
|
24
|
+
"SessionBootstrap",
|
|
25
|
+
"SessionConfig",
|
|
26
|
+
"VoxRtcControlSession",
|
|
27
|
+
"VoxRtcServerClient",
|
|
28
|
+
"WireEvent",
|
|
29
|
+
"__version__",
|
|
30
|
+
]
|
vox_rtc_server/client.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.request import Request, urlopen
|
|
9
|
+
|
|
10
|
+
from .session import VoxRtcControlSession
|
|
11
|
+
from .types import (
|
|
12
|
+
ConnectionState,
|
|
13
|
+
RTCIceServer,
|
|
14
|
+
SessionBootstrap,
|
|
15
|
+
SocketClientFactory,
|
|
16
|
+
SocketClientLike,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _normalize_base(base: str) -> str:
|
|
21
|
+
return base.rstrip("/")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _default_socket_base(http_base: str) -> str:
|
|
25
|
+
return f"{_normalize_base(http_base)}/v1/socket"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _to_bootstrap(data: Mapping[str, Any]) -> SessionBootstrap:
|
|
29
|
+
ice_servers = [
|
|
30
|
+
RTCIceServer(
|
|
31
|
+
urls=entry.get("urls", []),
|
|
32
|
+
username=entry.get("username"),
|
|
33
|
+
credential=entry.get("credential"),
|
|
34
|
+
)
|
|
35
|
+
for entry in data.get("ice_servers", [])
|
|
36
|
+
if isinstance(entry, Mapping)
|
|
37
|
+
]
|
|
38
|
+
return SessionBootstrap(
|
|
39
|
+
session_id=str(data["session_id"]),
|
|
40
|
+
client_token=str(data["client_token"]),
|
|
41
|
+
expires_at=str(data["expires_at"]),
|
|
42
|
+
join_token_ttl_seconds=int(data.get("join_token_ttl_seconds", 0)),
|
|
43
|
+
ice_servers=ice_servers,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _state_value(state: Any) -> str:
|
|
48
|
+
return str(getattr(state, "value", state))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _default_socket_factory(
|
|
52
|
+
endpoint: str,
|
|
53
|
+
params: Mapping[str, Any],
|
|
54
|
+
connection_timeout: float,
|
|
55
|
+
max_reconnect_delay: float,
|
|
56
|
+
) -> SocketClientLike:
|
|
57
|
+
try:
|
|
58
|
+
from pondsocket_client import ClientOptions, PondClient
|
|
59
|
+
except ImportError as exc:
|
|
60
|
+
raise ImportError(
|
|
61
|
+
"pondsocket-client is required for the default Vox RTC socket transport"
|
|
62
|
+
) from exc
|
|
63
|
+
|
|
64
|
+
options = ClientOptions(
|
|
65
|
+
connection_timeout=connection_timeout,
|
|
66
|
+
max_reconnect_delay=max_reconnect_delay,
|
|
67
|
+
)
|
|
68
|
+
return PondClient(endpoint, dict(params), options)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class VoxRtcServerClient:
|
|
72
|
+
__slots__ = (
|
|
73
|
+
"_connection_timeout",
|
|
74
|
+
"_http_base",
|
|
75
|
+
"_api_key",
|
|
76
|
+
"_join_timeout",
|
|
77
|
+
"_max_reconnect_delay",
|
|
78
|
+
"_request_timeout",
|
|
79
|
+
"_socket",
|
|
80
|
+
"_socket_base",
|
|
81
|
+
"_socket_factory",
|
|
82
|
+
"_socket_params",
|
|
83
|
+
"_urlopen",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
http_base: str,
|
|
90
|
+
api_key: str | None = None,
|
|
91
|
+
socket_base: str | None = None,
|
|
92
|
+
socket_params: Mapping[str, Any] | None = None,
|
|
93
|
+
connection_timeout: float = 10.0,
|
|
94
|
+
max_reconnect_delay: float = 30.0,
|
|
95
|
+
request_timeout: float = 15.0,
|
|
96
|
+
join_timeout: float = 10.0,
|
|
97
|
+
socket_factory: SocketClientFactory | None = None,
|
|
98
|
+
urlopen_impl: Any = urlopen,
|
|
99
|
+
) -> None:
|
|
100
|
+
self._http_base = _normalize_base(http_base)
|
|
101
|
+
resolved_api_key = (api_key if api_key is not None else os.environ.get("VOX_API_KEY", "")).strip()
|
|
102
|
+
self._api_key = resolved_api_key or None
|
|
103
|
+
self._socket_base = (
|
|
104
|
+
_normalize_base(socket_base) if socket_base else _default_socket_base(http_base)
|
|
105
|
+
)
|
|
106
|
+
merged_socket_params = dict(socket_params or {})
|
|
107
|
+
if self._api_key:
|
|
108
|
+
merged_socket_params["api_key"] = self._api_key
|
|
109
|
+
self._socket_params = merged_socket_params
|
|
110
|
+
self._connection_timeout = connection_timeout
|
|
111
|
+
self._max_reconnect_delay = max_reconnect_delay
|
|
112
|
+
self._request_timeout = request_timeout
|
|
113
|
+
self._join_timeout = join_timeout
|
|
114
|
+
self._socket_factory = socket_factory or _default_socket_factory
|
|
115
|
+
self._socket: SocketClientLike | None = None
|
|
116
|
+
self._urlopen = urlopen_impl
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def http_base(self) -> str:
|
|
120
|
+
return self._http_base
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def socket_base(self) -> str:
|
|
124
|
+
return self._socket_base
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def connection_state(self) -> ConnectionState:
|
|
128
|
+
if self._socket is None:
|
|
129
|
+
return ConnectionState.DISCONNECTED
|
|
130
|
+
raw = _state_value(self._socket.get_state())
|
|
131
|
+
return ConnectionState(raw)
|
|
132
|
+
|
|
133
|
+
async def connect(self) -> None:
|
|
134
|
+
socket = self._ensure_socket()
|
|
135
|
+
if _state_value(socket.get_state()) == ConnectionState.CONNECTED.value:
|
|
136
|
+
return
|
|
137
|
+
await socket.connect()
|
|
138
|
+
|
|
139
|
+
async def disconnect(self) -> None:
|
|
140
|
+
socket = self._socket
|
|
141
|
+
if socket is None:
|
|
142
|
+
return
|
|
143
|
+
await socket.disconnect()
|
|
144
|
+
self._socket = None
|
|
145
|
+
|
|
146
|
+
async def create_session(self) -> SessionBootstrap:
|
|
147
|
+
return await asyncio.to_thread(self._create_session_blocking)
|
|
148
|
+
|
|
149
|
+
async def attach_session(self, session_id: str) -> VoxRtcControlSession:
|
|
150
|
+
await self.connect()
|
|
151
|
+
socket = self._ensure_socket()
|
|
152
|
+
channel = socket.create_channel(f"/rtc/{session_id}", {})
|
|
153
|
+
session = VoxRtcControlSession(
|
|
154
|
+
channel,
|
|
155
|
+
session_id,
|
|
156
|
+
join_timeout=self._join_timeout,
|
|
157
|
+
)
|
|
158
|
+
await session.join()
|
|
159
|
+
return session
|
|
160
|
+
|
|
161
|
+
async def create_controlled_session(
|
|
162
|
+
self,
|
|
163
|
+
) -> tuple[SessionBootstrap, VoxRtcControlSession]:
|
|
164
|
+
bootstrap = await self.create_session()
|
|
165
|
+
session = await self.attach_session(bootstrap.session_id)
|
|
166
|
+
return bootstrap, session
|
|
167
|
+
|
|
168
|
+
def _ensure_socket(self) -> SocketClientLike:
|
|
169
|
+
if self._socket is None:
|
|
170
|
+
self._socket = self._socket_factory(
|
|
171
|
+
self._socket_base,
|
|
172
|
+
self._socket_params,
|
|
173
|
+
self._connection_timeout,
|
|
174
|
+
self._max_reconnect_delay,
|
|
175
|
+
)
|
|
176
|
+
return self._socket
|
|
177
|
+
|
|
178
|
+
def _create_session_blocking(self) -> SessionBootstrap:
|
|
179
|
+
request = Request(
|
|
180
|
+
f"{self._http_base}/v1/rtc/sessions",
|
|
181
|
+
data=json.dumps({}).encode("utf-8"),
|
|
182
|
+
headers={"content-type": "application/json"},
|
|
183
|
+
method="POST",
|
|
184
|
+
)
|
|
185
|
+
if self._api_key:
|
|
186
|
+
request.add_header("authorization", f"Bearer {self._api_key}")
|
|
187
|
+
with self._urlopen(request, timeout=self._request_timeout) as response:
|
|
188
|
+
status = int(response.status) if hasattr(response, "status") else int(response.getcode())
|
|
189
|
+
body = response.read().decode("utf-8")
|
|
190
|
+
if status < 200 or status >= 300:
|
|
191
|
+
raise RuntimeError(f"Failed to create Vox RTC session: {status} {body.strip()}")
|
|
192
|
+
return _to_bootstrap(json.loads(body))
|
vox_rtc_server/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .types import (
|
|
8
|
+
ChannelState,
|
|
9
|
+
ClientEventEnvelope,
|
|
10
|
+
ResponseOptions,
|
|
11
|
+
SessionConfig,
|
|
12
|
+
SocketChannelLike,
|
|
13
|
+
Unsubscribe,
|
|
14
|
+
WireEvent,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _state_value(state: Any) -> str:
|
|
19
|
+
return str(getattr(state, "value", state))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _payload_dict(payload: Any) -> dict[str, Any]:
|
|
23
|
+
if payload is None:
|
|
24
|
+
return {}
|
|
25
|
+
if isinstance(payload, dict):
|
|
26
|
+
return dict(payload)
|
|
27
|
+
if isinstance(payload, Mapping):
|
|
28
|
+
return dict(payload)
|
|
29
|
+
return {"payload": payload}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _response_options_payload(options: ResponseOptions | None) -> dict[str, Any]:
|
|
33
|
+
payload: dict[str, Any] = {}
|
|
34
|
+
if options is not None and options.allow_interruptions is not None:
|
|
35
|
+
payload["allow_interruptions"] = options.allow_interruptions
|
|
36
|
+
return payload
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VoxRtcControlSession:
|
|
40
|
+
__slots__ = ("_channel", "_channel_name", "_join_timeout", "_session_id")
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
channel: SocketChannelLike,
|
|
45
|
+
session_id: str,
|
|
46
|
+
*,
|
|
47
|
+
join_timeout: float = 10.0,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._channel = channel
|
|
50
|
+
self._session_id = session_id
|
|
51
|
+
self._channel_name = f"/rtc/{session_id}"
|
|
52
|
+
self._join_timeout = join_timeout
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def session_id(self) -> str:
|
|
56
|
+
return self._session_id
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def channel_name(self) -> str:
|
|
60
|
+
return self._channel_name
|
|
61
|
+
|
|
62
|
+
async def join(self) -> None:
|
|
63
|
+
loop = asyncio.get_running_loop()
|
|
64
|
+
done: asyncio.Future[None] = loop.create_future()
|
|
65
|
+
|
|
66
|
+
def handle_state(state: Any) -> None:
|
|
67
|
+
value = _state_value(state)
|
|
68
|
+
if value == ChannelState.JOINED.value and not done.done():
|
|
69
|
+
done.set_result(None)
|
|
70
|
+
elif value in (ChannelState.DECLINED.value, ChannelState.CLOSED.value) and not done.done():
|
|
71
|
+
done.set_exception(
|
|
72
|
+
RuntimeError(f"RTC channel join failed for {self._channel_name}: {value}")
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
unsubscribe = self._channel.on_channel_state_change(handle_state)
|
|
76
|
+
try:
|
|
77
|
+
self._channel.join()
|
|
78
|
+
await asyncio.wait_for(done, timeout=self._join_timeout)
|
|
79
|
+
finally:
|
|
80
|
+
unsubscribe()
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
self._channel.leave()
|
|
84
|
+
|
|
85
|
+
def on_event(self, handler: Callable[[WireEvent], None]) -> Unsubscribe:
|
|
86
|
+
def callback(message: Any) -> None:
|
|
87
|
+
handler(
|
|
88
|
+
WireEvent(
|
|
89
|
+
type=str(getattr(message, "event", "")),
|
|
90
|
+
data=_payload_dict(getattr(message, "payload", None)),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return self._channel.on_message(callback)
|
|
95
|
+
|
|
96
|
+
def on(self, event_name: str, handler: Callable[[dict[str, Any]], None]) -> Unsubscribe:
|
|
97
|
+
def callback(message: Any) -> None:
|
|
98
|
+
handler(_payload_dict(getattr(message, "payload", None)))
|
|
99
|
+
|
|
100
|
+
return self._channel.on_message_event(event_name, callback)
|
|
101
|
+
|
|
102
|
+
def send_control(self, event: str, payload: Mapping[str, Any] | None = None) -> None:
|
|
103
|
+
self._channel.send_message(event, dict(payload or {}))
|
|
104
|
+
|
|
105
|
+
def configure(self, config: SessionConfig) -> None:
|
|
106
|
+
session: dict[str, Any] = dict(config.extra)
|
|
107
|
+
if config.stt_model is not None:
|
|
108
|
+
session["stt_model"] = config.stt_model
|
|
109
|
+
if config.tts_model is not None:
|
|
110
|
+
session["tts_model"] = config.tts_model
|
|
111
|
+
if config.voice is not None:
|
|
112
|
+
session["voice"] = config.voice
|
|
113
|
+
if config.turn_profile is not None:
|
|
114
|
+
session["turn_profile"] = config.turn_profile
|
|
115
|
+
if config.vad_backend is not None:
|
|
116
|
+
session["vad_backend"] = config.vad_backend
|
|
117
|
+
if config.turn_detector is not None:
|
|
118
|
+
session["turn_detector"] = config.turn_detector
|
|
119
|
+
self.send_control("session.update", {"session": session})
|
|
120
|
+
|
|
121
|
+
def start_response(self, options: ResponseOptions | None = None) -> None:
|
|
122
|
+
self.send_control("response.start", _response_options_payload(options))
|
|
123
|
+
|
|
124
|
+
def append_response_text(
|
|
125
|
+
self,
|
|
126
|
+
delta: str,
|
|
127
|
+
options: ResponseOptions | None = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
payload = _response_options_payload(options)
|
|
130
|
+
payload["delta"] = delta
|
|
131
|
+
self.send_control("response.delta", payload)
|
|
132
|
+
|
|
133
|
+
def commit_response(self) -> None:
|
|
134
|
+
self.send_control("response.commit")
|
|
135
|
+
|
|
136
|
+
def cancel_response(self) -> None:
|
|
137
|
+
self.send_control("response.cancel")
|
|
138
|
+
|
|
139
|
+
def send_text_response(
|
|
140
|
+
self,
|
|
141
|
+
text: str,
|
|
142
|
+
options: ResponseOptions | None = None,
|
|
143
|
+
*,
|
|
144
|
+
cancel_first: bool = True,
|
|
145
|
+
) -> None:
|
|
146
|
+
if cancel_first:
|
|
147
|
+
self.cancel_response()
|
|
148
|
+
self.start_response(options)
|
|
149
|
+
self.append_response_text(text, options)
|
|
150
|
+
self.commit_response()
|
|
151
|
+
|
|
152
|
+
def send_client_event(self, envelope: ClientEventEnvelope) -> None:
|
|
153
|
+
self.send_control(
|
|
154
|
+
"client.event",
|
|
155
|
+
{"event": envelope.event, "payload": envelope.payload},
|
|
156
|
+
)
|
vox_rtc_server/types.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Any, Protocol, TypeAlias
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConnectionState(StrEnum):
|
|
10
|
+
DISCONNECTED = "disconnected"
|
|
11
|
+
CONNECTING = "connecting"
|
|
12
|
+
CONNECTED = "connected"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChannelState(StrEnum):
|
|
16
|
+
IDLE = "IDLE"
|
|
17
|
+
JOINING = "JOINING"
|
|
18
|
+
JOINED = "JOINED"
|
|
19
|
+
STALLED = "STALLED"
|
|
20
|
+
CLOSED = "CLOSED"
|
|
21
|
+
DECLINED = "DECLINED"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class RTCIceServer:
|
|
26
|
+
urls: str | list[str]
|
|
27
|
+
username: str | None = None
|
|
28
|
+
credential: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class SessionBootstrap:
|
|
33
|
+
session_id: str
|
|
34
|
+
client_token: str
|
|
35
|
+
expires_at: str
|
|
36
|
+
join_token_ttl_seconds: int
|
|
37
|
+
ice_servers: list[RTCIceServer] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(slots=True)
|
|
41
|
+
class SessionConfig:
|
|
42
|
+
stt_model: str | None = None
|
|
43
|
+
tts_model: str | None = None
|
|
44
|
+
voice: str | None = None
|
|
45
|
+
turn_profile: str | None = None
|
|
46
|
+
vad_backend: str | None = None
|
|
47
|
+
turn_detector: str | None = None
|
|
48
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(slots=True)
|
|
52
|
+
class ResponseOptions:
|
|
53
|
+
allow_interruptions: bool | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(slots=True)
|
|
57
|
+
class ClientEventEnvelope:
|
|
58
|
+
event: str
|
|
59
|
+
payload: Any = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(slots=True)
|
|
63
|
+
class WireEvent:
|
|
64
|
+
type: str
|
|
65
|
+
data: dict[str, Any]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
Unsubscribe: TypeAlias = Callable[[], None]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ServerMessageLike(Protocol):
|
|
72
|
+
@property
|
|
73
|
+
def event(self) -> str: ...
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def payload(self) -> Any: ...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SocketChannelLike(Protocol):
|
|
80
|
+
def join(self) -> None: ...
|
|
81
|
+
|
|
82
|
+
def leave(self) -> None: ...
|
|
83
|
+
|
|
84
|
+
def send_message(self, event: str, payload: Mapping[str, Any] | None = None) -> None: ...
|
|
85
|
+
|
|
86
|
+
def on_message(self, callback: Callable[[ServerMessageLike], None]) -> Unsubscribe: ...
|
|
87
|
+
|
|
88
|
+
def on_message_event(
|
|
89
|
+
self, event_name: str, callback: Callable[[ServerMessageLike], None]
|
|
90
|
+
) -> Unsubscribe: ...
|
|
91
|
+
|
|
92
|
+
def on_channel_state_change(
|
|
93
|
+
self, callback: Callable[[Any], None]
|
|
94
|
+
) -> Unsubscribe: ...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SocketClientLike(Protocol):
|
|
98
|
+
async def connect(self) -> None: ...
|
|
99
|
+
|
|
100
|
+
async def disconnect(self) -> None: ...
|
|
101
|
+
|
|
102
|
+
def get_state(self) -> Any: ...
|
|
103
|
+
|
|
104
|
+
def create_channel(
|
|
105
|
+
self, name: str, params: Mapping[str, Any] | None = None
|
|
106
|
+
) -> SocketChannelLike: ...
|
|
107
|
+
|
|
108
|
+
def on_connection_change(self, callback: Callable[[Any], None]) -> Unsubscribe: ...
|
|
109
|
+
|
|
110
|
+
def on_error(self, callback: Callable[[BaseException], None]) -> Unsubscribe: ...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
SocketClientFactory: TypeAlias = Callable[
|
|
114
|
+
[str, Mapping[str, Any], float, float],
|
|
115
|
+
SocketClientLike,
|
|
116
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vox-rtc-server
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Server-side Python SDK for Vox-hosted WebRTC sessions.
|
|
5
|
+
Author: eleven-am
|
|
6
|
+
License: GPL-3.0-or-later
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# `vox-rtc-server`
|
|
11
|
+
|
|
12
|
+
Server-side Python SDK for Vox-hosted WebRTC sessions.
|
|
13
|
+
|
|
14
|
+
This package is for backend applications that need to:
|
|
15
|
+
|
|
16
|
+
- create RTC sessions over HTTP
|
|
17
|
+
- attach to `/v1/socket`
|
|
18
|
+
- join `/rtc/{session_id}`
|
|
19
|
+
- send `session.update`, `response.*`, and `client.event`
|
|
20
|
+
- observe RTC control events
|
|
21
|
+
|
|
22
|
+
It is intentionally narrow. It is not the general STT/TTS/text SDK.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install vox-rtc-server pondsocket-client
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The SDK uses the PondSocket Python client for the control-plane socket.
|
|
31
|
+
Until `pondsocket-client` is published, install it from your local checkout or git ref.
|
|
32
|
+
Authentication can be passed explicitly with `api_key=...` or through `VOX_API_KEY`.
|
|
33
|
+
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import asyncio
|
|
38
|
+
import os
|
|
39
|
+
|
|
40
|
+
from vox_rtc_server import (
|
|
41
|
+
ClientEventEnvelope,
|
|
42
|
+
SessionConfig,
|
|
43
|
+
VoxRtcServerClient,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def main() -> None:
|
|
48
|
+
client = VoxRtcServerClient(
|
|
49
|
+
http_base="https://vox.example.com",
|
|
50
|
+
api_key=os.environ.get("VOX_API_KEY"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
bootstrap, session = await client.create_controlled_session()
|
|
54
|
+
print("session:", bootstrap.session_id)
|
|
55
|
+
|
|
56
|
+
session.on_event(lambda event: print(event.type, event.data))
|
|
57
|
+
|
|
58
|
+
session.configure(
|
|
59
|
+
SessionConfig(
|
|
60
|
+
stt_model="parakeet-stt-onnx:tdt-0.6b-v3",
|
|
61
|
+
tts_model="kokoro-tts-onnx:v1.0",
|
|
62
|
+
voice="af_heart",
|
|
63
|
+
turn_profile="browser_default",
|
|
64
|
+
vad_backend="silero",
|
|
65
|
+
turn_detector="livekit",
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
session.send_text_response("Hello from Python.")
|
|
70
|
+
session.send_client_event(
|
|
71
|
+
ClientEventEnvelope(
|
|
72
|
+
event="render.url",
|
|
73
|
+
payload={"url": "https://example.com"},
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
vox_rtc_server/__init__.py,sha256=bUpCrq4dkT_V9gkkFPgsujgvTzUny9pjTxrrVkxw3ig,585
|
|
2
|
+
vox_rtc_server/client.py,sha256=O3tw5ixLge0ru8ajpEkKJ4k9EHyMvPLNhyNcZ7DwQ7I,6219
|
|
3
|
+
vox_rtc_server/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
4
|
+
vox_rtc_server/session.py,sha256=fQasNrcuLzpizdVJTwiigou9D4LSr6tx9KE7mq8n_fA,5087
|
|
5
|
+
vox_rtc_server/types.py,sha256=36J5U8NMFSMjWjR4K6gbOC2fz4bKe-CE3ES18oJACxI,2675
|
|
6
|
+
vox_rtc_server-0.1.1.dist-info/METADATA,sha256=It5BLjSMa1GUqjDWexaYP1CzJLUScSU05SggLZ1ZG-0,1953
|
|
7
|
+
vox_rtc_server-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
vox_rtc_server-0.1.1.dist-info/RECORD,,
|