livekit-plugins-spatialreal 1.3.12__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.
- livekit_plugins_spatialreal-1.3.12/.gitignore +216 -0
- livekit_plugins_spatialreal-1.3.12/LICENSE +21 -0
- livekit_plugins_spatialreal-1.3.12/PKG-INFO +135 -0
- livekit_plugins_spatialreal-1.3.12/README.md +109 -0
- livekit_plugins_spatialreal-1.3.12/livekit/plugins/spatialreal/__init__.py +37 -0
- livekit_plugins_spatialreal-1.3.12/livekit/plugins/spatialreal/avatar.py +278 -0
- livekit_plugins_spatialreal-1.3.12/livekit/plugins/spatialreal/log.py +3 -0
- livekit_plugins_spatialreal-1.3.12/livekit/plugins/spatialreal/py.typed +0 -0
- livekit_plugins_spatialreal-1.3.12/livekit/plugins/spatialreal/version.py +1 -0
- livekit_plugins_spatialreal-1.3.12/pyproject.toml +57 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
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
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
|
|
204
|
+
# Ruff stuff:
|
|
205
|
+
.ruff_cache/
|
|
206
|
+
|
|
207
|
+
# PyPI configuration file
|
|
208
|
+
.pypirc
|
|
209
|
+
|
|
210
|
+
# Marimo
|
|
211
|
+
marimo/_static/
|
|
212
|
+
marimo/_lsp/
|
|
213
|
+
__marimo__/
|
|
214
|
+
|
|
215
|
+
# Streamlit
|
|
216
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 spatialwalk
|
|
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,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: livekit-plugins-spatialreal
|
|
3
|
+
Version: 1.3.12
|
|
4
|
+
Summary: Agent Framework plugin for SpatialReal Avatar
|
|
5
|
+
Project-URL: Documentation, https://docs.spatialreal.com
|
|
6
|
+
Project-URL: Website, https://spatialreal.com/
|
|
7
|
+
Project-URL: Source, https://github.com/spatialreal/livekit-plugins-spatialreal
|
|
8
|
+
Author-email: 3DRX <3drxkjy@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,audio,avatar,livekit,realtime,spatialreal,video,voice,webrtc
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.9.0
|
|
23
|
+
Requires-Dist: avatarkit>=0.1.3
|
|
24
|
+
Requires-Dist: livekit-agents>=1.2.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# LiveKit Agents Plugin for SpatialReal Avatar
|
|
28
|
+
|
|
29
|
+
This plugin provides integration with [SpatialReal](https://spatialreal.com)'s avatar service for lip-synced avatar rendering in LiveKit voice agents.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install livekit-plugins-spatialreal
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or install from source:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install -e .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
Set the following environment variables:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Required
|
|
49
|
+
SPATIALREAL_API_KEY=your-api-key
|
|
50
|
+
SPATIALREAL_APP_ID=your-app-id
|
|
51
|
+
SPATIALREAL_AVATAR_ID=your-avatar-id
|
|
52
|
+
|
|
53
|
+
# Optional
|
|
54
|
+
SPATIALREAL_CONSOLE_ENDPOINT=
|
|
55
|
+
SPATIALREAL_INGRESS_ENDPOINT=
|
|
56
|
+
|
|
57
|
+
# LiveKit credentials
|
|
58
|
+
LIVEKIT_URL=
|
|
59
|
+
LIVEKIT_API_KEY=
|
|
60
|
+
LIVEKIT_API_SECRET=
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from livekit.agents import Agent, AgentSession, JobContext, cli, WorkerOptions
|
|
67
|
+
from livekit.plugins import spatialreal
|
|
68
|
+
|
|
69
|
+
class VoiceAssistant(Agent):
|
|
70
|
+
def __init__(self):
|
|
71
|
+
super().__init__(
|
|
72
|
+
instructions="You are a helpful voice assistant."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def entrypoint(ctx: JobContext):
|
|
76
|
+
await ctx.connect()
|
|
77
|
+
|
|
78
|
+
# Configure your pipeline components (VAD, STT, LLM, TTS)
|
|
79
|
+
session = AgentSession(
|
|
80
|
+
vad=vad,
|
|
81
|
+
stt=stt,
|
|
82
|
+
llm=llm,
|
|
83
|
+
tts=tts,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Initialize and start the avatar session
|
|
87
|
+
avatar = spatialreal.AvatarSession()
|
|
88
|
+
await avatar.start(session, room=ctx.room)
|
|
89
|
+
|
|
90
|
+
# Start the agent session
|
|
91
|
+
await session.start(
|
|
92
|
+
agent=VoiceAssistant(),
|
|
93
|
+
room=ctx.room,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## API Reference
|
|
101
|
+
|
|
102
|
+
### `AvatarSession`
|
|
103
|
+
|
|
104
|
+
Main class for integrating SpatialReal avatars with LiveKit agents.
|
|
105
|
+
|
|
106
|
+
#### Constructor Parameters
|
|
107
|
+
|
|
108
|
+
| Parameter | Type | Description |
|
|
109
|
+
|-----------|------|-------------|
|
|
110
|
+
| `api_key` | `str` | SpatialReal API key (or set `SPATIALREAL_API_KEY`) |
|
|
111
|
+
| `app_id` | `str` | SpatialReal application ID (or set `SPATIALREAL_APP_ID`) |
|
|
112
|
+
| `avatar_id` | `str` | Avatar ID to use (or set `SPATIALREAL_AVATAR_ID`) |
|
|
113
|
+
| `console_endpoint_url` | `str` | Custom console endpoint URL |
|
|
114
|
+
| `ingress_endpoint_url` | `str` | Custom ingress endpoint URL |
|
|
115
|
+
| `avatar_participant_identity` | `str` | LiveKit identity for avatar participant |
|
|
116
|
+
|
|
117
|
+
#### Methods
|
|
118
|
+
|
|
119
|
+
- `start(agent_session, room, *, livekit_url, livekit_api_key, livekit_api_secret)`: Start the avatar session and hook into the agent's audio output.
|
|
120
|
+
- `aclose()`: Clean up avatar session resources.
|
|
121
|
+
|
|
122
|
+
### `SpatialRealException`
|
|
123
|
+
|
|
124
|
+
Exception raised for SpatialReal-related errors.
|
|
125
|
+
|
|
126
|
+
## How It Works
|
|
127
|
+
|
|
128
|
+
1. The plugin intercepts TTS audio output from the agent session
|
|
129
|
+
2. Audio frames are forwarded to SpatialReal's avatar service
|
|
130
|
+
3. SpatialReal generates lip-synced video and audio
|
|
131
|
+
4. The avatar joins the LiveKit room and publishes the synchronized streams
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# LiveKit Agents Plugin for SpatialReal Avatar
|
|
2
|
+
|
|
3
|
+
This plugin provides integration with [SpatialReal](https://spatialreal.com)'s avatar service for lip-synced avatar rendering in LiveKit voice agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install livekit-plugins-spatialreal
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Set the following environment variables:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Required
|
|
23
|
+
SPATIALREAL_API_KEY=your-api-key
|
|
24
|
+
SPATIALREAL_APP_ID=your-app-id
|
|
25
|
+
SPATIALREAL_AVATAR_ID=your-avatar-id
|
|
26
|
+
|
|
27
|
+
# Optional
|
|
28
|
+
SPATIALREAL_CONSOLE_ENDPOINT=
|
|
29
|
+
SPATIALREAL_INGRESS_ENDPOINT=
|
|
30
|
+
|
|
31
|
+
# LiveKit credentials
|
|
32
|
+
LIVEKIT_URL=
|
|
33
|
+
LIVEKIT_API_KEY=
|
|
34
|
+
LIVEKIT_API_SECRET=
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from livekit.agents import Agent, AgentSession, JobContext, cli, WorkerOptions
|
|
41
|
+
from livekit.plugins import spatialreal
|
|
42
|
+
|
|
43
|
+
class VoiceAssistant(Agent):
|
|
44
|
+
def __init__(self):
|
|
45
|
+
super().__init__(
|
|
46
|
+
instructions="You are a helpful voice assistant."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def entrypoint(ctx: JobContext):
|
|
50
|
+
await ctx.connect()
|
|
51
|
+
|
|
52
|
+
# Configure your pipeline components (VAD, STT, LLM, TTS)
|
|
53
|
+
session = AgentSession(
|
|
54
|
+
vad=vad,
|
|
55
|
+
stt=stt,
|
|
56
|
+
llm=llm,
|
|
57
|
+
tts=tts,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Initialize and start the avatar session
|
|
61
|
+
avatar = spatialreal.AvatarSession()
|
|
62
|
+
await avatar.start(session, room=ctx.room)
|
|
63
|
+
|
|
64
|
+
# Start the agent session
|
|
65
|
+
await session.start(
|
|
66
|
+
agent=VoiceAssistant(),
|
|
67
|
+
room=ctx.room,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API Reference
|
|
75
|
+
|
|
76
|
+
### `AvatarSession`
|
|
77
|
+
|
|
78
|
+
Main class for integrating SpatialReal avatars with LiveKit agents.
|
|
79
|
+
|
|
80
|
+
#### Constructor Parameters
|
|
81
|
+
|
|
82
|
+
| Parameter | Type | Description |
|
|
83
|
+
|-----------|------|-------------|
|
|
84
|
+
| `api_key` | `str` | SpatialReal API key (or set `SPATIALREAL_API_KEY`) |
|
|
85
|
+
| `app_id` | `str` | SpatialReal application ID (or set `SPATIALREAL_APP_ID`) |
|
|
86
|
+
| `avatar_id` | `str` | Avatar ID to use (or set `SPATIALREAL_AVATAR_ID`) |
|
|
87
|
+
| `console_endpoint_url` | `str` | Custom console endpoint URL |
|
|
88
|
+
| `ingress_endpoint_url` | `str` | Custom ingress endpoint URL |
|
|
89
|
+
| `avatar_participant_identity` | `str` | LiveKit identity for avatar participant |
|
|
90
|
+
|
|
91
|
+
#### Methods
|
|
92
|
+
|
|
93
|
+
- `start(agent_session, room, *, livekit_url, livekit_api_key, livekit_api_secret)`: Start the avatar session and hook into the agent's audio output.
|
|
94
|
+
- `aclose()`: Clean up avatar session resources.
|
|
95
|
+
|
|
96
|
+
### `SpatialRealException`
|
|
97
|
+
|
|
98
|
+
Exception raised for SpatialReal-related errors.
|
|
99
|
+
|
|
100
|
+
## How It Works
|
|
101
|
+
|
|
102
|
+
1. The plugin intercepts TTS audio output from the agent session
|
|
103
|
+
2. Audio frames are forwarded to SpatialReal's avatar service
|
|
104
|
+
3. SpatialReal generates lip-synced video and audio
|
|
105
|
+
4. The avatar joins the LiveKit room and publishes the synchronized streams
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""SpatialReal avatar plugin for LiveKit Agents.
|
|
2
|
+
|
|
3
|
+
This plugin provides integration with SpatialReal's avatar service for
|
|
4
|
+
lip-synced avatar rendering in LiveKit voice agents.
|
|
5
|
+
|
|
6
|
+
See https://docs.spatialreal.com for more information.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from livekit.plugins.spatialreal import AvatarSession
|
|
10
|
+
|
|
11
|
+
avatar = AvatarSession()
|
|
12
|
+
await avatar.start(agent_session, room=ctx.room)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .avatar import AvatarSession, SpatialRealException
|
|
16
|
+
from .version import __version__
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AvatarSession",
|
|
20
|
+
"SpatialRealException",
|
|
21
|
+
"__version__",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Try to register plugin if Plugin class is available (livekit-agents >= 1.3)
|
|
25
|
+
try:
|
|
26
|
+
from livekit.agents import Plugin
|
|
27
|
+
|
|
28
|
+
from .log import logger
|
|
29
|
+
|
|
30
|
+
class SpatialRealPlugin(Plugin):
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
super().__init__(__name__, __version__, __package__, logger)
|
|
33
|
+
|
|
34
|
+
Plugin.register_plugin(SpatialRealPlugin())
|
|
35
|
+
except (ImportError, AttributeError):
|
|
36
|
+
# Plugin registration not available in older versions
|
|
37
|
+
pass
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SpatialReal Avatar integration for LiveKit Agents.
|
|
3
|
+
|
|
4
|
+
This module provides AvatarSession which hooks into an AgentSession
|
|
5
|
+
to route TTS audio to the SpatialReal avatar service.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
|
|
14
|
+
from avatarkit import (
|
|
15
|
+
AvatarSession as AvatarkitSession,
|
|
16
|
+
)
|
|
17
|
+
from avatarkit import (
|
|
18
|
+
LiveKitEgressConfig,
|
|
19
|
+
new_avatar_session,
|
|
20
|
+
)
|
|
21
|
+
from livekit.agents import AgentSession
|
|
22
|
+
from livekit.agents.voice.avatar import AudioSegmentEnd, QueueAudioOutput
|
|
23
|
+
|
|
24
|
+
from livekit import rtc
|
|
25
|
+
|
|
26
|
+
from .log import logger
|
|
27
|
+
|
|
28
|
+
__all__ = ["AvatarSession", "SpatialRealException"]
|
|
29
|
+
|
|
30
|
+
DEFAULT_AVATAR_PARTICIPANT_IDENTITY = "spatialreal-avatar"
|
|
31
|
+
DEFAULT_SAMPLE_RATE = 24000
|
|
32
|
+
|
|
33
|
+
# Default endpoints (China)
|
|
34
|
+
DEFAULT_CONSOLE_ENDPOINT = "https://console.us-west.spatialwalk.cloud/v1/console"
|
|
35
|
+
DEFAULT_INGRESS_ENDPOINT = "wss://api.us-west.spatialwalk.cloud/v2/driveningress"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SpatialRealException(Exception):
|
|
39
|
+
"""Exception raised for SpatialReal-related errors."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AvatarSession:
|
|
45
|
+
"""
|
|
46
|
+
This connects to SpatialReal's avatar service and routes TTS audio
|
|
47
|
+
from the agent to the avatar for lip-synced rendering. The avatar
|
|
48
|
+
service joins the LiveKit room and publishes synchronized video + audio.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
api_key: SpatialReal API key. Falls back to SPATIALREAL_API_KEY env var.
|
|
52
|
+
app_id: SpatialReal application ID. Falls back to SPATIALREAL_APP_ID env var.
|
|
53
|
+
avatar_id: Avatar ID to use. Falls back to SPATIALREAL_AVATAR_ID env var.
|
|
54
|
+
console_endpoint_url: Console endpoint URL. Falls back to
|
|
55
|
+
SPATIALREAL_CONSOLE_ENDPOINT env var or default.
|
|
56
|
+
ingress_endpoint_url: Ingress endpoint URL. Falls back to
|
|
57
|
+
SPATIALREAL_INGRESS_ENDPOINT env var or default.
|
|
58
|
+
avatar_participant_identity: LiveKit identity for the avatar participant.
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
avatar = AvatarSession()
|
|
62
|
+
await avatar.start(session, room=ctx.room)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
api_key: str | None = None,
|
|
69
|
+
app_id: str | None = None,
|
|
70
|
+
avatar_id: str | None = None,
|
|
71
|
+
console_endpoint_url: str | None = None,
|
|
72
|
+
ingress_endpoint_url: str | None = None,
|
|
73
|
+
avatar_participant_identity: str | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
# Resolve API key
|
|
76
|
+
self._api_key = api_key or os.getenv("SPATIALREAL_API_KEY")
|
|
77
|
+
if not self._api_key:
|
|
78
|
+
raise SpatialRealException(
|
|
79
|
+
"api_key must be provided or SPATIALREAL_API_KEY environment variable must be set"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Resolve app ID
|
|
83
|
+
self._app_id = app_id or os.getenv("SPATIALREAL_APP_ID")
|
|
84
|
+
if not self._app_id:
|
|
85
|
+
raise SpatialRealException("app_id must be provided or SPATIALREAL_APP_ID environment variable must be set")
|
|
86
|
+
|
|
87
|
+
# Resolve avatar ID
|
|
88
|
+
self._avatar_id = avatar_id or os.getenv("SPATIALREAL_AVATAR_ID")
|
|
89
|
+
if not self._avatar_id:
|
|
90
|
+
raise SpatialRealException(
|
|
91
|
+
"avatar_id must be provided or SPATIALREAL_AVATAR_ID environment variable must be set"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Resolve endpoints
|
|
95
|
+
self._console_endpoint_url = (
|
|
96
|
+
console_endpoint_url or os.getenv("SPATIALREAL_CONSOLE_ENDPOINT") or DEFAULT_CONSOLE_ENDPOINT
|
|
97
|
+
)
|
|
98
|
+
self._ingress_endpoint_url = (
|
|
99
|
+
ingress_endpoint_url or os.getenv("SPATIALREAL_INGRESS_ENDPOINT") or DEFAULT_INGRESS_ENDPOINT
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Avatar participant configuration
|
|
103
|
+
self._avatar_participant_identity = avatar_participant_identity or DEFAULT_AVATAR_PARTICIPANT_IDENTITY
|
|
104
|
+
|
|
105
|
+
# Internal state
|
|
106
|
+
self._avatarkit_session: AvatarkitSession | None = None
|
|
107
|
+
self._agent_session: AgentSession | None = None
|
|
108
|
+
self._audio_buffer: QueueAudioOutput | None = None
|
|
109
|
+
self._main_task: asyncio.Task | None = None
|
|
110
|
+
self._initialized = False
|
|
111
|
+
|
|
112
|
+
async def start(
|
|
113
|
+
self,
|
|
114
|
+
agent_session: AgentSession,
|
|
115
|
+
room: rtc.Room,
|
|
116
|
+
*,
|
|
117
|
+
livekit_url: str | None = None,
|
|
118
|
+
livekit_api_key: str | None = None,
|
|
119
|
+
livekit_api_secret: str | None = None,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Start the avatar session and hook into the agent session.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
agent_session: The AgentSession to hook into for TTS audio.
|
|
126
|
+
room: The LiveKit room for egress configuration.
|
|
127
|
+
livekit_url: LiveKit server URL. Falls back to LIVEKIT_URL env var.
|
|
128
|
+
livekit_api_key: LiveKit API key. Falls back to LIVEKIT_API_KEY env var.
|
|
129
|
+
livekit_api_secret: LiveKit API secret. Falls back to LIVEKIT_API_SECRET env var.
|
|
130
|
+
"""
|
|
131
|
+
if self._initialized:
|
|
132
|
+
logger.warning("Avatar session already initialized")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
self._agent_session = agent_session
|
|
136
|
+
|
|
137
|
+
# Resolve LiveKit credentials
|
|
138
|
+
lk_url = livekit_url or os.getenv("LIVEKIT_URL")
|
|
139
|
+
lk_api_key = livekit_api_key or os.getenv("LIVEKIT_API_KEY")
|
|
140
|
+
lk_api_secret = livekit_api_secret or os.getenv("LIVEKIT_API_SECRET")
|
|
141
|
+
|
|
142
|
+
if not lk_url or not lk_api_key or not lk_api_secret:
|
|
143
|
+
raise SpatialRealException(
|
|
144
|
+
"livekit_url, livekit_api_key, and livekit_api_secret must be provided "
|
|
145
|
+
"or LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET environment variables must be set"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
room_name = room.name
|
|
149
|
+
logger.info(f"Initializing SpatialReal avatar session for room: {room_name}")
|
|
150
|
+
logger.debug(f"Console endpoint: {self._console_endpoint_url}")
|
|
151
|
+
logger.debug(f"Ingress endpoint: {self._ingress_endpoint_url}")
|
|
152
|
+
|
|
153
|
+
# Create LiveKit egress configuration for the avatar to join the room
|
|
154
|
+
livekit_egress = LiveKitEgressConfig(
|
|
155
|
+
url=lk_url,
|
|
156
|
+
api_key=lk_api_key,
|
|
157
|
+
api_secret=lk_api_secret,
|
|
158
|
+
room_name=room_name,
|
|
159
|
+
publisher_id=self._avatar_participant_identity,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Create avatar session with LiveKit egress mode
|
|
163
|
+
self._avatarkit_session = new_avatar_session(
|
|
164
|
+
api_key=self._api_key,
|
|
165
|
+
app_id=self._app_id,
|
|
166
|
+
avatar_id=self._avatar_id,
|
|
167
|
+
console_endpoint_url=self._console_endpoint_url,
|
|
168
|
+
ingress_endpoint_url=self._ingress_endpoint_url,
|
|
169
|
+
expire_at=datetime.now(timezone.utc) + timedelta(hours=1),
|
|
170
|
+
livekit_egress=livekit_egress,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Initialize and start the avatar session
|
|
174
|
+
await self._avatarkit_session.init()
|
|
175
|
+
await self._avatarkit_session.start()
|
|
176
|
+
logger.info("SpatialReal avatar session connected")
|
|
177
|
+
|
|
178
|
+
# Create audio buffer using livekit-agents' QueueAudioOutput
|
|
179
|
+
sample_rate = agent_session.tts.sample_rate if agent_session.tts else DEFAULT_SAMPLE_RATE
|
|
180
|
+
self._audio_buffer = QueueAudioOutput(sample_rate=sample_rate)
|
|
181
|
+
|
|
182
|
+
# Hook into agent session's audio output
|
|
183
|
+
agent_session.output.audio = self._audio_buffer
|
|
184
|
+
|
|
185
|
+
# Start the audio buffer
|
|
186
|
+
await self._audio_buffer.start()
|
|
187
|
+
|
|
188
|
+
# Register for clear_buffer events (interruptions)
|
|
189
|
+
@self._audio_buffer.on("clear_buffer")
|
|
190
|
+
def on_clear_buffer() -> None:
|
|
191
|
+
asyncio.create_task(self._handle_interrupt())
|
|
192
|
+
|
|
193
|
+
# Start the main task that forwards audio to avatar
|
|
194
|
+
self._main_task = asyncio.create_task(self._run_main_task())
|
|
195
|
+
|
|
196
|
+
self._initialized = True
|
|
197
|
+
logger.info("Avatar audio output attached to agent session")
|
|
198
|
+
|
|
199
|
+
# Register cleanup on session close
|
|
200
|
+
@agent_session.on("close")
|
|
201
|
+
def on_session_close() -> None:
|
|
202
|
+
asyncio.create_task(self.aclose())
|
|
203
|
+
|
|
204
|
+
async def _run_main_task(self) -> None:
|
|
205
|
+
"""Main task that forwards audio from the buffer to the avatar service."""
|
|
206
|
+
if not self._audio_buffer or not self._avatarkit_session:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
frame_count = 0
|
|
211
|
+
async for item in self._audio_buffer:
|
|
212
|
+
if isinstance(item, rtc.AudioFrame):
|
|
213
|
+
# Convert AudioFrame to bytes and send to avatar
|
|
214
|
+
audio_bytes = bytes(item.data)
|
|
215
|
+
frame_count += 1
|
|
216
|
+
|
|
217
|
+
if frame_count == 1:
|
|
218
|
+
logger.debug("Avatar: First audio frame received")
|
|
219
|
+
|
|
220
|
+
await self._avatarkit_session.send_audio(
|
|
221
|
+
audio=audio_bytes,
|
|
222
|
+
end=False,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
elif isinstance(item, AudioSegmentEnd):
|
|
226
|
+
# End of audio segment - signal completion to avatar
|
|
227
|
+
logger.debug(f"Avatar: Segment end, sent {frame_count} frames")
|
|
228
|
+
await self._avatarkit_session.send_audio(
|
|
229
|
+
audio=b"",
|
|
230
|
+
end=True,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Notify the buffer that playback is finished
|
|
234
|
+
self._audio_buffer.notify_playback_finished(
|
|
235
|
+
playback_position=0.0,
|
|
236
|
+
interrupted=False,
|
|
237
|
+
)
|
|
238
|
+
frame_count = 0
|
|
239
|
+
|
|
240
|
+
except asyncio.CancelledError:
|
|
241
|
+
logger.debug("Avatar main task cancelled")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.error(f"Error in avatar main task: {e}")
|
|
244
|
+
|
|
245
|
+
async def _handle_interrupt(self) -> None:
|
|
246
|
+
"""Handle interruption - stop avatar's current audio processing."""
|
|
247
|
+
if not self._avatarkit_session:
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
interrupted_id = await self._avatarkit_session.interrupt()
|
|
252
|
+
logger.debug(f"Avatar interrupted, request_id={interrupted_id}")
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.warning(f"Failed to interrupt avatar: {e}")
|
|
255
|
+
|
|
256
|
+
async def aclose(self) -> None:
|
|
257
|
+
"""Clean up avatar session resources."""
|
|
258
|
+
if self._main_task:
|
|
259
|
+
self._main_task.cancel()
|
|
260
|
+
try:
|
|
261
|
+
await self._main_task
|
|
262
|
+
except asyncio.CancelledError:
|
|
263
|
+
pass
|
|
264
|
+
self._main_task = None
|
|
265
|
+
|
|
266
|
+
if self._audio_buffer:
|
|
267
|
+
await self._audio_buffer.aclose()
|
|
268
|
+
self._audio_buffer = None
|
|
269
|
+
|
|
270
|
+
if self._avatarkit_session:
|
|
271
|
+
try:
|
|
272
|
+
await self._avatarkit_session.close()
|
|
273
|
+
logger.info("Avatar session closed")
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.warning(f"Error closing avatar session: {e}")
|
|
276
|
+
finally:
|
|
277
|
+
self._avatarkit_session = None
|
|
278
|
+
self._initialized = False
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.12"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "livekit-plugins-spatialreal"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Agent Framework plugin for SpatialReal Avatar"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9.0"
|
|
12
|
+
authors = [{ name = "3DRX", email = "3drxkjy@gmail.com" }]
|
|
13
|
+
keywords = ["voice", "ai", "realtime", "audio", "video", "livekit", "webrtc", "avatar", "spatialreal"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
18
|
+
"Topic :: Multimedia :: Video",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"livekit-agents>=1.2.9",
|
|
28
|
+
"avatarkit>=0.1.3",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Documentation = "https://docs.spatialreal.com"
|
|
33
|
+
Website = "https://spatialreal.com/"
|
|
34
|
+
Source = "https://github.com/spatialreal/livekit-plugins-spatialreal"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.version]
|
|
37
|
+
path = "livekit/plugins/spatialreal/version.py"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["livekit"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = ["/livekit"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
target-version = "py39"
|
|
47
|
+
line-length = 120
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = [
|
|
51
|
+
"E", # pycodestyle errors
|
|
52
|
+
"W", # pycodestyle warnings
|
|
53
|
+
"F", # pyflakes
|
|
54
|
+
"I", # isort
|
|
55
|
+
"UP", # pyupgrade
|
|
56
|
+
"B", # flake8-bugbear
|
|
57
|
+
]
|