Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions livekit-plugins/livekit-plugins-bey/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# LiveKit Plugins Beyond Presence

Agent Framework Plugin for human avatars with [Beyond Presence](https://docs.bey.dev)'s API.

Currently supports speech to video.

## Installation

```bash
pip install livekit-plugins-bey
```

## Pre-requisites

Create a developer API key from the [creator dashboard](https://app.bey.chat) and set the `BEY_API_KEY` environment variable with it:

```bash
export BEY_API_KEY=<your-bey-api-key>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from .core import (
API_KEY_ENV_VAR,
API_URL_ENV_VAR,
EGE_STOCK_AVATAR_ID,
BeyAvatarSession,
BeyException,
start_bey_avatar_session,
)
from .version import __version__

__all__ = [
"API_KEY_ENV_VAR",
"API_URL_ENV_VAR",
"EGE_STOCK_AVATAR_ID",
"BeyAvatarSession",
"BeyException",
"start_bey_avatar_session",
"__version__",
]

from livekit.agents import Plugin

from .log import logger


class BeyPlugin(Plugin):
def __init__(self) -> None:
super().__init__(__name__, __version__, __package__, logger)


Plugin.register_plugin(BeyPlugin())
144 changes: 144 additions & 0 deletions livekit-plugins/livekit-plugins-bey/livekit/plugins/bey/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from __future__ import annotations

import os
from collections.abc import Awaitable

import httpx

from livekit import api, rtc
from livekit.agents import (
JobContext,
)
from livekit.agents.voice.avatar import DataStreamAudioOutput
from livekit.agents.voice.io import AudioOutput
from livekit.agents.voice.room_io import ATTRIBUTE_PUBLISH_ON_BEHALF, RoomOutputOptions

API_URL_ENV_VAR = "BEY_API_URL"
"""
The environment variable name for the Beyond Presence API URL
"""

API_KEY_ENV_VAR = "BEY_API_KEY"
"""
The environment variable name for the Beyond Presence API key
"""


EGE_STOCK_AVATAR_ID = "b9be11b8-89fb-4227-8f86-4a881393cbdb"
"""
The ID of Ege's stock avatar
"""

_DEFAULT_API_URL = "https://api.bey.dev"

_AVATAR_AGENT_IDENTITY = "bey-avatar-agent"
_AVATAR_AGENT_NAME = "bey-avatar-agent"


class BeyAvatarSession:
"""A Beyond Presence avatar session"""

def __init__(
self,
avatar_agent_joined_awaitable: Awaitable[None],
local_agent_audio_output: AudioOutput,
local_agent_room_output_options: RoomOutputOptions,
) -> None:
"""
Args:
avatar_agent_joined_awaitable: An awaitable that resolves when the avatar agent joins
the room
local_agent_audio_output: The audio sink for the local agent output audio to reach the
avatar agent
local_agent_room_output_options: The room output options for the local agent to write
messages as the avatar agent
"""
self._avatar_agent_joined_awaitable = avatar_agent_joined_awaitable
self._local_agent_audio_output = local_agent_audio_output
self._local_agent_room_output_options = local_agent_room_output_options

async def wait_for_avatar_agent(self) -> None:
"""Wait for the avatar agent to join the room"""
await self._avatar_agent_joined_awaitable

@property
def local_agent_audio_output(self) -> AudioOutput:
"""The audio sink for the local agent output audio to reach the avatar agent"""
return self._local_agent_audio_output

@property
def local_agent_room_output_options(self) -> RoomOutputOptions:
"""The room output options for the local agent to write messages as the avatar agent"""
return self._local_agent_room_output_options


class BeyException(Exception):
"""Exception for Beyond Presence errors"""


async def start_bey_avatar_session(
ctx: JobContext,
avatar_id: str = EGE_STOCK_AVATAR_ID,
) -> BeyAvatarSession:
"""
Start a Beyond Presence avatar session

Args:
ctx: The LiveKit Agent job context
avatar_id: The ID of the avatar to request

Returns:
The context for the Beyond Presence avatar session

Raises:
BeyException: If the Beyond Presence session fails to start
"""

if (api_key := os.environ.get(API_KEY_ENV_VAR)) is None:
raise BeyException(f"{API_KEY_ENV_VAR} environment variable not set")

api_url = os.environ.get(API_URL_ENV_VAR, _DEFAULT_API_URL)

livekit_avatar_token = (
api.AccessToken()
.with_kind("agent")
.with_identity(_AVATAR_AGENT_IDENTITY)
.with_name(_AVATAR_AGENT_NAME)
.with_grants(api.VideoGrants(room_join=True, room=ctx.room.name))
# allow the avatar agent to publish audio and video on behalf of your local agent
.with_attributes({ATTRIBUTE_PUBLISH_ON_BEHALF: ctx.room.local_participant.identity})
.to_jwt()
)

async with httpx.AsyncClient() as client:
response = await client.post(
f"{api_url}/v1/session",
headers={
"x-api-key": api_key,
},
json={
"avatar_id": avatar_id,
"livekit_url": ctx._info.url,
"livekit_token": livekit_avatar_token,
},
)
if response.is_error:
raise BeyException(f"Avatar session server responded with error: {response.text}")

async def wait_for_participant() -> None:
await ctx.wait_for_participant(
identity=_AVATAR_AGENT_IDENTITY,
kind=rtc.ParticipantKind.PARTICIPANT_KIND_AGENT,
)

return BeyAvatarSession(
avatar_agent_joined_awaitable=wait_for_participant(),
local_agent_audio_output=DataStreamAudioOutput(
ctx.room, destination_identity=_AVATAR_AGENT_IDENTITY
),
local_agent_room_output_options=RoomOutputOptions(
# avatar agent will speak to the user, so disable audio output on our end
audio_enabled=False,
transcription_enabled=True,
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import logging

logger = logging.getLogger("livekit.plugins.bey")
15 changes: 15 additions & 0 deletions livekit-plugins/livekit-plugins-bey/livekit/plugins/bey/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2025 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

__version__ = "1.0.11"
39 changes: 39 additions & 0 deletions livekit-plugins/livekit-plugins-bey/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "livekit-plugins-bey"
dynamic = ["version"]
description = "Agent Framework plugin for services from Beyond Presence"
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.9.0"
authors = [{ name = "LiveKit", email = "support@livekit.io" }]
keywords = ["webrtc", "realtime", "audio", "video", "livekit"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add avatar here?

classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Topic :: Multimedia :: Sound/Audio",
"Topic :: Multimedia :: Video",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3 :: Only",
]
dependencies = ["livekit-agents>=1.0.11", "httpx"]

[project.urls]
Documentation = "https://docs.livekit.io"
Website = "https://livekit.io/"
Source = "https://github.com/livekit/agents"

[tool.hatch.version]
path = "livekit/plugins/bey/version.py"

[tool.hatch.build.targets.wheel]
packages = ["livekit"]

[tool.hatch.build.targets.sdist]
include = ["/livekit"]
Loading