livekit-plugins-spatialreal 1.4.4__tar.gz → 1.4.5__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.4.4 → livekit_plugins_spatialreal-1.4.5}/PKG-INFO +2 -2
- {livekit_plugins_spatialreal-1.4.4 → livekit_plugins_spatialreal-1.4.5}/livekit/plugins/spatialreal/__init__.py +1 -1
- {livekit_plugins_spatialreal-1.4.4 → livekit_plugins_spatialreal-1.4.5}/livekit/plugins/spatialreal/avatar.py +100 -19
- livekit_plugins_spatialreal-1.4.5/livekit/plugins/spatialreal/version.py +1 -0
- {livekit_plugins_spatialreal-1.4.4 → livekit_plugins_spatialreal-1.4.5}/pyproject.toml +1 -1
- livekit_plugins_spatialreal-1.4.4/livekit/plugins/spatialreal/version.py +0 -1
- {livekit_plugins_spatialreal-1.4.4 → livekit_plugins_spatialreal-1.4.5}/.gitignore +0 -0
- {livekit_plugins_spatialreal-1.4.4 → livekit_plugins_spatialreal-1.4.5}/LICENSE +0 -0
- {livekit_plugins_spatialreal-1.4.4 → livekit_plugins_spatialreal-1.4.5}/README.md +0 -0
- {livekit_plugins_spatialreal-1.4.4 → livekit_plugins_spatialreal-1.4.5}/livekit/plugins/spatialreal/log.py +0 -0
- {livekit_plugins_spatialreal-1.4.4 → livekit_plugins_spatialreal-1.4.5}/livekit/plugins/spatialreal/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: livekit-plugins-spatialreal
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.5
|
|
4
4
|
Summary: Agent Framework plugin for SpatialReal Avatar
|
|
5
5
|
Project-URL: Documentation, https://docs.spatialreal.com
|
|
6
6
|
Project-URL: Website, https://www.spatialreal.ai/
|
|
@@ -22,7 +22,7 @@ Classifier: Topic :: Multimedia :: Sound/Audio
|
|
|
22
22
|
Classifier: Topic :: Multimedia :: Video
|
|
23
23
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
24
|
Requires-Python: >=3.10.0
|
|
25
|
-
Requires-Dist: avatarkit>=0.1.
|
|
25
|
+
Requires-Dist: avatarkit>=0.1.7
|
|
26
26
|
Requires-Dist: livekit-agents>=1.2.9
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This plugin provides integration with SpatialReal's avatar service for
|
|
4
4
|
lip-synced avatar rendering in LiveKit voice agents.
|
|
5
5
|
|
|
6
|
-
See https://docs.spatialreal.
|
|
6
|
+
See https://docs.spatialreal.ai for more information.
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
from livekit.plugins.spatialreal import AvatarSession
|
|
@@ -23,8 +23,9 @@ from avatarkit import (
|
|
|
23
23
|
new_avatar_session,
|
|
24
24
|
)
|
|
25
25
|
from avatarkit.proto.generated import message_pb2 as _message_pb2
|
|
26
|
-
from livekit.agents import AgentSession, UserStateChangedEvent
|
|
26
|
+
from livekit.agents import AgentSession, UserStateChangedEvent, get_job_context
|
|
27
27
|
from livekit.agents.voice.avatar import AudioSegmentEnd, QueueAudioOutput
|
|
28
|
+
from livekit.agents.voice.room_io import ATTRIBUTE_PUBLISH_ON_BEHALF
|
|
28
29
|
|
|
29
30
|
from livekit import rtc
|
|
30
31
|
|
|
@@ -39,6 +40,8 @@ DEFAULT_SAMPLE_RATE = 24000
|
|
|
39
40
|
MIN_COMPLETION_TIMEOUT_SECONDS = 3.0
|
|
40
41
|
COMPLETION_TIMEOUT_BUFFER_SECONDS = 2.0
|
|
41
42
|
ACTIVE_SEGMENT_IDLE_END_SECONDS = 1.0
|
|
43
|
+
DEFAULT_SESSION_TTL = timedelta(hours=1)
|
|
44
|
+
LIVEKIT_AVATAR_PUBLISH_SOURCES = ["camera", "microphone"]
|
|
42
45
|
|
|
43
46
|
DEFAULT_CONSOLE_ENDPOINT = "https://console.us-west.spatialwalk.cloud/v1/console"
|
|
44
47
|
DEFAULT_INGRESS_ENDPOINT = "wss://api.us-west.spatialwalk.cloud/v2/driveningress"
|
|
@@ -175,25 +178,36 @@ class AvatarSession:
|
|
|
175
178
|
lk_api_key = livekit_api_key or os.getenv("LIVEKIT_API_KEY")
|
|
176
179
|
lk_api_secret = livekit_api_secret or os.getenv("LIVEKIT_API_SECRET")
|
|
177
180
|
|
|
178
|
-
if not lk_url
|
|
181
|
+
if not lk_url:
|
|
182
|
+
raise SpatialRealException("livekit_url must be provided or LIVEKIT_URL environment variable must be set")
|
|
183
|
+
|
|
184
|
+
if not lk_api_key or not lk_api_secret:
|
|
179
185
|
raise SpatialRealException(
|
|
180
|
-
"
|
|
181
|
-
"or
|
|
186
|
+
"livekit_api_key and livekit_api_secret must be provided "
|
|
187
|
+
"or LIVEKIT_API_KEY and LIVEKIT_API_SECRET environment variables must be set"
|
|
182
188
|
)
|
|
183
189
|
|
|
184
190
|
room_name = room.name
|
|
185
|
-
agent_participant_identity = room
|
|
191
|
+
agent_participant_identity = self._resolve_local_participant_identity(room)
|
|
186
192
|
logger.info(f"Initializing SpatialReal avatar session for room: {room_name}")
|
|
187
193
|
logger.debug(f"Console endpoint: {self._console_endpoint_url}")
|
|
188
194
|
logger.debug(f"Ingress endpoint: {self._ingress_endpoint_url}")
|
|
189
195
|
|
|
190
|
-
egress_attributes = {
|
|
196
|
+
egress_attributes = {ATTRIBUTE_PUBLISH_ON_BEHALF: agent_participant_identity}
|
|
197
|
+
session_expire_at = datetime.now(timezone.utc) + DEFAULT_SESSION_TTL
|
|
198
|
+
resolved_lk_api_token = self._generate_livekit_api_token(
|
|
199
|
+
room_name=room_name,
|
|
200
|
+
livekit_api_key=lk_api_key,
|
|
201
|
+
livekit_api_secret=lk_api_secret,
|
|
202
|
+
publisher_identity=self._avatar_participant_identity,
|
|
203
|
+
publisher_attributes=egress_attributes,
|
|
204
|
+
ttl=DEFAULT_SESSION_TTL,
|
|
205
|
+
)
|
|
191
206
|
|
|
192
207
|
# Create LiveKit egress configuration for the avatar to join the room
|
|
193
208
|
livekit_egress_kwargs: dict[str, Any] = {
|
|
194
209
|
"url": lk_url,
|
|
195
|
-
"
|
|
196
|
-
"api_secret": lk_api_secret,
|
|
210
|
+
"api_token": resolved_lk_api_token,
|
|
197
211
|
"room_name": room_name,
|
|
198
212
|
"publisher_id": self._avatar_participant_identity,
|
|
199
213
|
"extra_attributes": egress_attributes,
|
|
@@ -218,7 +232,7 @@ class AvatarSession:
|
|
|
218
232
|
avatar_id=self._avatar_id,
|
|
219
233
|
console_endpoint_url=self._console_endpoint_url,
|
|
220
234
|
ingress_endpoint_url=self._ingress_endpoint_url,
|
|
221
|
-
expire_at=
|
|
235
|
+
expire_at=session_expire_at,
|
|
222
236
|
livekit_egress=livekit_egress,
|
|
223
237
|
sample_rate=resolved_sample_rate,
|
|
224
238
|
transport_frames=self._on_transport_frame,
|
|
@@ -283,11 +297,64 @@ class AvatarSession:
|
|
|
283
297
|
reason = self._format_error_reason(error)
|
|
284
298
|
return (
|
|
285
299
|
"Failed to start SpatialReal avatar session. "
|
|
286
|
-
"Check SpatialReal credentials,
|
|
300
|
+
"Check SpatialReal credentials, LiveKit room auth/token configuration, "
|
|
301
|
+
"endpoint URLs, and outbound network access. "
|
|
287
302
|
f"room={room_name}, avatar_id={self._avatar_id}, ingress_endpoint_url={self._ingress_endpoint_url}, "
|
|
288
303
|
f"sample_rate={sample_rate}. Reason: {reason}"
|
|
289
304
|
)
|
|
290
305
|
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _generate_livekit_api_token(
|
|
308
|
+
*,
|
|
309
|
+
room_name: str,
|
|
310
|
+
livekit_api_key: str,
|
|
311
|
+
livekit_api_secret: str,
|
|
312
|
+
publisher_identity: str,
|
|
313
|
+
publisher_attributes: dict[str, str],
|
|
314
|
+
ttl: timedelta,
|
|
315
|
+
) -> str:
|
|
316
|
+
try:
|
|
317
|
+
from livekit import api as livekit_api
|
|
318
|
+
except ImportError as exc:
|
|
319
|
+
raise SpatialRealException(
|
|
320
|
+
"livekit-api must be installed to generate LiveKit access tokens for avatar egress"
|
|
321
|
+
) from exc
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
return (
|
|
325
|
+
livekit_api.AccessToken(livekit_api_key, livekit_api_secret)
|
|
326
|
+
.with_kind("agent")
|
|
327
|
+
.with_identity(publisher_identity)
|
|
328
|
+
.with_name(publisher_identity)
|
|
329
|
+
.with_ttl(ttl)
|
|
330
|
+
.with_attributes(publisher_attributes)
|
|
331
|
+
.with_grants(
|
|
332
|
+
livekit_api.VideoGrants(
|
|
333
|
+
room_join=True,
|
|
334
|
+
room=room_name,
|
|
335
|
+
can_subscribe=False,
|
|
336
|
+
can_publish_data=False,
|
|
337
|
+
can_publish_sources=LIVEKIT_AVATAR_PUBLISH_SOURCES,
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
.to_jwt()
|
|
341
|
+
)
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
raise SpatialRealException(
|
|
344
|
+
"Failed to generate LiveKit access token for avatar worker. "
|
|
345
|
+
f"room={room_name}, publisher_identity={publisher_identity}. "
|
|
346
|
+
f"Reason: {AvatarSession._format_error_reason(exc)}"
|
|
347
|
+
) from exc
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def _resolve_local_participant_identity(room: rtc.Room) -> str:
|
|
351
|
+
try:
|
|
352
|
+
return get_job_context().token_claims().identity
|
|
353
|
+
except RuntimeError as exc:
|
|
354
|
+
if not room.isconnected():
|
|
355
|
+
raise SpatialRealException("failed to get local participant identity") from exc
|
|
356
|
+
return room.local_participant.identity
|
|
357
|
+
|
|
291
358
|
@staticmethod
|
|
292
359
|
def _format_error_reason(error: BaseException) -> str:
|
|
293
360
|
root_error = error
|
|
@@ -420,12 +487,18 @@ class AvatarSession:
|
|
|
420
487
|
active_segment = self._segments.pop(active_req_id, None)
|
|
421
488
|
segment = self._segments.get(req_id)
|
|
422
489
|
|
|
490
|
+
if active_segment is None and segment is None:
|
|
491
|
+
logger.debug(
|
|
492
|
+
"Avatar: Segment completed before finalization finished "
|
|
493
|
+
f"(request_id={active_req_id}, finalize_request_id={req_id}, source={source})"
|
|
494
|
+
)
|
|
495
|
+
return True
|
|
496
|
+
|
|
423
497
|
if segment is None:
|
|
424
|
-
if active_segment is
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
segment = _SegmentState(req_id=req_id)
|
|
498
|
+
if active_segment is None:
|
|
499
|
+
return True
|
|
500
|
+
active_segment.req_id = req_id
|
|
501
|
+
segment = active_segment
|
|
429
502
|
self._segments[req_id] = segment
|
|
430
503
|
elif active_segment is not None and segment is not active_segment:
|
|
431
504
|
segment.pushed_duration = max(segment.pushed_duration, active_segment.pushed_duration)
|
|
@@ -574,10 +647,18 @@ class AvatarSession:
|
|
|
574
647
|
try:
|
|
575
648
|
interrupted_id = await self._avatarkit_session.interrupt()
|
|
576
649
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
self.
|
|
650
|
+
async with self._segment_finalize_lock:
|
|
651
|
+
if not self._complete_segment(req_id=interrupted_id, interrupted=True, reason="interrupt"):
|
|
652
|
+
# Fallback: a race can leave the request id unmatched.
|
|
653
|
+
if self._active_req_id is not None:
|
|
654
|
+
self._complete_segment(
|
|
655
|
+
req_id=self._active_req_id,
|
|
656
|
+
interrupted=True,
|
|
657
|
+
reason="interrupt_fallback",
|
|
658
|
+
)
|
|
659
|
+
# Complete any remaining pending segments that were also interrupted
|
|
660
|
+
for req_id in list(self._segments.keys()):
|
|
661
|
+
self._complete_segment(req_id=req_id, interrupted=True, reason="interrupt_remaining")
|
|
581
662
|
|
|
582
663
|
logger.debug(f"Avatar interrupted, request_id={interrupted_id}")
|
|
583
664
|
except Exception as e:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.5"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.4.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|