livekit-plugins-did 1.5.3__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.
@@ -0,0 +1,42 @@
1
+ # Copyright 2025 LiveKit, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """D-ID virtual avatar plugin for LiveKit Agents
16
+
17
+ See https://docs.livekit.io/agents/integrations/avatar/did/ for more information.
18
+ """
19
+
20
+ from .avatar import AvatarSession
21
+ from .errors import DIDException
22
+ from .types import AudioConfig
23
+ from .version import __version__
24
+
25
+ __all__ = [
26
+ "AudioConfig",
27
+ "AvatarSession",
28
+ "DIDException",
29
+ "__version__",
30
+ ]
31
+
32
+ from livekit.agents import Plugin
33
+
34
+ from .log import logger
35
+
36
+
37
+ class DIDPlugin(Plugin):
38
+ def __init__(self) -> None:
39
+ super().__init__(__name__, __version__, __package__, logger)
40
+
41
+
42
+ Plugin.register_plugin(DIDPlugin())
@@ -0,0 +1,92 @@
1
+ import asyncio
2
+ import os
3
+ from typing import Any
4
+
5
+ import aiohttp
6
+
7
+ from livekit.agents import (
8
+ DEFAULT_API_CONNECT_OPTIONS,
9
+ NOT_GIVEN,
10
+ APIConnectionError,
11
+ APIConnectOptions,
12
+ APIStatusError,
13
+ NotGivenOr,
14
+ utils,
15
+ )
16
+
17
+ from .errors import DIDException
18
+ from .log import logger
19
+
20
+ DEFAULT_API_URL = "https://api.d-id.com"
21
+
22
+
23
+ class DIDAPI:
24
+ def __init__(
25
+ self,
26
+ api_key: NotGivenOr[str] = NOT_GIVEN,
27
+ api_url: NotGivenOr[str] = NOT_GIVEN,
28
+ *,
29
+ conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
30
+ session: aiohttp.ClientSession | None = None,
31
+ ) -> None:
32
+ did_api_key = api_key if utils.is_given(api_key) else os.getenv("DID_API_KEY")
33
+ if not did_api_key:
34
+ raise DIDException("DID_API_KEY must be set")
35
+ self._api_key = did_api_key
36
+
37
+ self._api_url = api_url if utils.is_given(api_url) else DEFAULT_API_URL
38
+ self._conn_options = conn_options
39
+ self._session = session or aiohttp.ClientSession()
40
+
41
+ async def join_session(
42
+ self,
43
+ *,
44
+ agent_id: str,
45
+ transport: dict[str, Any],
46
+ audio_config: dict[str, Any],
47
+ ) -> str:
48
+ """Dispatch a D-ID avatar worker into the room.
49
+
50
+ Returns the session id.
51
+ """
52
+ payload: dict[str, Any] = {
53
+ "transport": transport,
54
+ "audio_config": audio_config,
55
+ }
56
+ response_data = await self._post(f"v2/agents/{agent_id}/sessions/join", payload)
57
+ return response_data["id"] # type: ignore
58
+
59
+ async def _post(self, endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
60
+ url = f"{self._api_url}/{endpoint}"
61
+ num_attempts = self._conn_options.max_retry + 1
62
+ for attempt in range(num_attempts):
63
+ try:
64
+ async with self._session.post(
65
+ url,
66
+ headers={
67
+ "Content-Type": "application/json",
68
+ "Authorization": f"Basic {self._api_key}",
69
+ },
70
+ json=payload,
71
+ timeout=aiohttp.ClientTimeout(sock_connect=self._conn_options.timeout),
72
+ ) as response:
73
+ if not response.ok:
74
+ text = await response.text()
75
+ raise APIStatusError(
76
+ "D-ID API error",
77
+ status_code=response.status,
78
+ body=text,
79
+ )
80
+ return await response.json() # type: ignore
81
+ except APIStatusError:
82
+ raise
83
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
84
+ logger.warning(
85
+ f"D-ID API request failed (attempt {attempt + 1}/{num_attempts})",
86
+ extra={"error": str(e)},
87
+ )
88
+ if attempt == num_attempts - 1:
89
+ raise APIConnectionError(f"Failed to connect to D-ID API at {url}") from e
90
+ await asyncio.sleep(self._conn_options.retry_interval)
91
+
92
+ raise APIConnectionError(f"Failed to connect to D-ID API at {url}")
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import aiohttp
6
+
7
+ from livekit import api, rtc
8
+ from livekit.agents import (
9
+ DEFAULT_API_CONNECT_OPTIONS,
10
+ NOT_GIVEN,
11
+ AgentSession,
12
+ APIConnectOptions,
13
+ NotGivenOr,
14
+ get_job_context,
15
+ utils,
16
+ )
17
+ from livekit.agents.voice.avatar import DataStreamAudioOutput
18
+ from livekit.agents.voice.room_io import ATTRIBUTE_PUBLISH_ON_BEHALF
19
+
20
+ from .api import DIDAPI
21
+ from .errors import DIDException
22
+ from .log import logger
23
+ from .types import AudioConfig
24
+
25
+ _AVATAR_AGENT_IDENTITY = "d-id-avatar-agent"
26
+ _AVATAR_AGENT_NAME = "d-id-avatar-agent"
27
+
28
+
29
+ class AvatarSession:
30
+ """A D-ID avatar session"""
31
+
32
+ def __init__(
33
+ self,
34
+ *,
35
+ agent_id: str,
36
+ api_url: NotGivenOr[str] = NOT_GIVEN,
37
+ api_key: NotGivenOr[str] = NOT_GIVEN,
38
+ audio_config: AudioConfig | None = None,
39
+ avatar_participant_identity: NotGivenOr[str] = NOT_GIVEN,
40
+ avatar_participant_name: NotGivenOr[str] = NOT_GIVEN,
41
+ conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
42
+ ) -> None:
43
+ self._http_session: aiohttp.ClientSession | None = None
44
+ self._conn_options = conn_options
45
+ self._agent_id = agent_id
46
+ self._audio_config = audio_config or AudioConfig()
47
+ self.session_id: str | None = None
48
+
49
+ self._api = DIDAPI(
50
+ api_url=api_url,
51
+ api_key=api_key,
52
+ conn_options=conn_options,
53
+ session=self._ensure_http_session(),
54
+ )
55
+
56
+ self._avatar_participant_identity = (
57
+ avatar_participant_identity
58
+ if utils.is_given(avatar_participant_identity)
59
+ else _AVATAR_AGENT_IDENTITY
60
+ )
61
+ self._avatar_participant_name = (
62
+ avatar_participant_name
63
+ if utils.is_given(avatar_participant_name)
64
+ else _AVATAR_AGENT_NAME
65
+ )
66
+
67
+ def _ensure_http_session(self) -> aiohttp.ClientSession:
68
+ if self._http_session is None:
69
+ self._http_session = utils.http_context.http_session()
70
+
71
+ return self._http_session
72
+
73
+ async def start(
74
+ self,
75
+ agent_session: AgentSession,
76
+ room: rtc.Room,
77
+ *,
78
+ livekit_url: NotGivenOr[str] = NOT_GIVEN,
79
+ livekit_api_key: NotGivenOr[str] = NOT_GIVEN,
80
+ livekit_api_secret: NotGivenOr[str] = NOT_GIVEN,
81
+ ) -> None:
82
+ _livekit_url = livekit_url if utils.is_given(livekit_url) else os.getenv("LIVEKIT_URL")
83
+ _livekit_api_key = (
84
+ livekit_api_key if utils.is_given(livekit_api_key) else os.getenv("LIVEKIT_API_KEY")
85
+ )
86
+ _livekit_api_secret = (
87
+ livekit_api_secret
88
+ if utils.is_given(livekit_api_secret)
89
+ else os.getenv("LIVEKIT_API_SECRET")
90
+ )
91
+ if not _livekit_url or not _livekit_api_key or not _livekit_api_secret:
92
+ raise DIDException(
93
+ "livekit_url, livekit_api_key, and livekit_api_secret must be set "
94
+ "by arguments or environment variables"
95
+ )
96
+
97
+ job_ctx = get_job_context()
98
+ local_participant_identity = job_ctx.local_participant_identity
99
+ livekit_token = (
100
+ api.AccessToken(api_key=_livekit_api_key, api_secret=_livekit_api_secret)
101
+ .with_kind("agent")
102
+ .with_identity(self._avatar_participant_identity)
103
+ .with_name(self._avatar_participant_name)
104
+ .with_grants(api.VideoGrants(room_join=True, room=room.name))
105
+ .with_attributes({ATTRIBUTE_PUBLISH_ON_BEHALF: local_participant_identity})
106
+ .to_jwt()
107
+ )
108
+
109
+ logger.debug("starting avatar session")
110
+ self.session_id = await self._api.join_session(
111
+ agent_id=self._agent_id,
112
+ transport={
113
+ "provider": "livekit",
114
+ "server_url": _livekit_url,
115
+ "token": livekit_token,
116
+ "room_name": room.name,
117
+ },
118
+ audio_config={"sample_rate": self._audio_config.sample_rate},
119
+ )
120
+
121
+ agent_session.output.audio = DataStreamAudioOutput(
122
+ room=room,
123
+ destination_identity=self._avatar_participant_identity,
124
+ sample_rate=self._audio_config.sample_rate,
125
+ wait_remote_track=rtc.TrackKind.KIND_VIDEO,
126
+ )
@@ -0,0 +1,4 @@
1
+ class DIDException(Exception):
2
+ """Custom exception for D-ID API errors."""
3
+
4
+ pass
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("livekit.plugins.did")
File without changes
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass
2
+
3
+ DEFAULT_SAMPLE_RATE = 24000
4
+
5
+
6
+ @dataclass
7
+ class AudioConfig:
8
+ """Configuration for the audio sent to the D-ID avatar.
9
+
10
+ Attributes:
11
+ sample_rate: Sample rate in Hz. Supported values: 16000, 24000, 48000.
12
+ """
13
+
14
+ sample_rate: int = DEFAULT_SAMPLE_RATE
@@ -0,0 +1,15 @@
1
+ # Copyright 2025 LiveKit, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ __version__ = "1.5.3"
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: livekit-plugins-did
3
+ Version: 1.5.3
4
+ Summary: Agent Framework plugin for D-ID avatar
5
+ Project-URL: Documentation, https://docs.livekit.io
6
+ Project-URL: Website, https://livekit.io/
7
+ Project-URL: Source, https://github.com/livekit/agents
8
+ Author-email: LiveKit <support@livekit.io>
9
+ License-Expression: Apache-2.0
10
+ Keywords: ai,audio,avatar,d-id,livekit,realtime,video,voice,webrtc
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Topic :: Multimedia :: Sound/Audio
17
+ Classifier: Topic :: Multimedia :: Video
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10.0
20
+ Requires-Dist: livekit-agents>=1.5.3
21
+ Description-Content-Type: text/markdown
22
+
23
+ # D-ID plugin for LiveKit Agents
24
+
25
+ Support for the [D-ID](https://d-id.com/) virtual avatar.
26
+
27
+ See the [D-ID integration docs](https://docs.livekit.io/agents/models/avatar/plugins/did/) for more information.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install livekit-plugins-did
33
+ ```
34
+
35
+ ## Pre-requisites
36
+
37
+ You'll need an API key from D-ID. It can be set as an environment variable: `DID_API_KEY`
38
+
39
+ ## Supported avatars
40
+
41
+ This plugin only supports **v4 avatars** (type: `expressive`). Earlier avatar versions are not compatible. See the [D-ID Create Agent API](https://docs.d-id.com/reference/createagent) for details on creating a compatible agent.
42
+
43
+ Example — creating an expressive agent via the D-ID API:
44
+ ```bash
45
+ curl -X POST https://api.d-id.com/agents \
46
+ -H "Authorization: Basic <YOUR_API_KEY>" \
47
+ -H "Content-Type: application/json" \
48
+ -d '{
49
+ "presenter": {
50
+ "type": "expressive",
51
+ "presenter_id": "public_mia_elegant@avt_TJ0Tq5"
52
+ },
53
+ "preview_name": "My Expressive Agent"
54
+ }'
55
+ ```
56
+
57
+ Use the agent ID from the response as the `agent_id` parameter in the plugin.
@@ -0,0 +1,11 @@
1
+ livekit/plugins/did/__init__.py,sha256=QuGig4xR7xKXpM_n1uixaabp0uqn6D4pAFwEp1bSpVE,1166
2
+ livekit/plugins/did/api.py,sha256=WjeIBboFf7IFtXuH3upKaZC3l23FmRm4udOOLLG3ibk,3176
3
+ livekit/plugins/did/avatar.py,sha256=jx8Rnpw_Jr4C5e8S7hiq7SSKypB-EJlqpEbtZP5_Lzk,4292
4
+ livekit/plugins/did/errors.py,sha256=a3Gr4xVhcRGJt-GY1L6wpZki1wUSy3QcNOeLZFkg5F0,89
5
+ livekit/plugins/did/log.py,sha256=N6IlaU8EgsmLBqbAon-M1n4NKzb2-U8mbDrvrwPRrBs,66
6
+ livekit/plugins/did/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ livekit/plugins/did/types.py,sha256=AG7AQ0jEK0lNnC14XbMG4qvY2UcnEGPmytNZZ3lYU4o,303
8
+ livekit/plugins/did/version.py,sha256=kLVGYefaKk-jJ2kGUkAxNZU_39gUUT1ZrBBgq-5C3zE,600
9
+ livekit_plugins_did-1.5.3.dist-info/METADATA,sha256=_BNPkfcJ_s2F3y_Df5g1jKEQCIBBA5SxQChyMWDhSYg,1997
10
+ livekit_plugins_did-1.5.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ livekit_plugins_did-1.5.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any