Package videosdk.plugins.simli

Sub-modules

videosdk.plugins.simli.simli

Classes

class SimliAvatar (config: simli.simli.SimliConfig,
simli_url: str = 'https://api.simli.ai',
is_trinity_avatar: bool = False)
Expand source code
class SimliAvatar:
    def __init__(
        self,
        config: SimliConfig,
        simli_url: str = DEFAULT_SIMLI_HTTP_URL,
        is_trinity_avatar: bool = False,
    ):
        """Initialize the Simli Avatar plugin.

        Args:
            config (SimliConfig): The configuration for the Simli avatar.
            simli_url (str): The Simli API URL. Defaults to "https://api.simli.ai".
            is_trinity_avatar (bool): Specify if the face id in the simli config is a trinity avatar to ensure synchronization
        """
        super().__init__()
        self.config = config
        self._stream_start_time = None
        self.video_track: SimliVideoTrack | None = None
        self.video_receiver_track: VideoStreamTrack | None = None
        self.audio_track: SimliAudioTrack | None = None
        self.audio_receiver_track: AudioStreamTrack | None = None
        self.run = True
        self._is_speaking = False
        self._avatar_speaking = False
        self._last_error = None
        self._stopping = False
        self._keep_alive_task = None
        self._last_audio_time = 0
        self._is_trinity_avatar = is_trinity_avatar
        self.simli_url = simli_url

    async def connect(self):
        loop = asyncio.get_event_loop()
        await self._initialize_connection()
        self.audio_track = SimliAudioTrack(loop, self.simli_client)
        self.video_track = SimliVideoTrack(self._is_trinity_avatar)
        if self._stream_start_time is None:
            self._stream_start_time = time.time()

        self._last_audio_time = time.time()
        self._keep_alive_task = asyncio.create_task(self._keep_alive_loop())

    async def mark_silent(self):
        self._avatar_speaking = False

    async def mark_speaking(self):
        self._avatar_speaking = True

    async def _initialize_connection(self):
        """Initialize connection with retry logic"""
        self.simli_client = SimliClient(self.config, True, 0, self.simli_url)
        await self.simli_client.Initialize()
        while not hasattr(self.simli_client, "audioReceiver"):
            await asyncio.sleep(0.0001)
        self._register_track(self.simli_client.audioReceiver)
        self._register_track(self.simli_client.videoReceiver)
        self.simli_client.registerSilentEventCallback(self.mark_silent)
        self.simli_client.registerSpeakEventCallback(self.mark_speaking)

    def _register_track(self, track: MediaStreamTrack):
        if track.kind == "video":
            self.video_receiver_track: VideoStreamTrack = track
            asyncio.ensure_future(self._process_video_frames())
        elif track.kind == "audio":
            self.audio_receiver_track: AudioStreamTrack = track
            asyncio.ensure_future(self._process_audio_frames())

    async def _process_video_frames(self):
        """Simple video frame processing for real-time playback"""

        while self.run and not self._stopping:
            try:
                frame: VideoFrame = await self.video_receiver_track.recv()
                if frame is None:
                    continue
                self.video_track.add_frame(frame)
            except Exception as e:
                logger.error(f"Simli: Video processing error: {e}")
                if not self.run or self._stopping:
                    break
                await asyncio.sleep(0.1)
                continue

    async def _process_audio_frames(self):
        """Simple audio frame processing for real-time playback"""

        while self.run and not self._stopping:
            try:
                frame: AudioFrame = await self.audio_receiver_track.recv()
                if frame is None:
                    logger.warning("Simli: Received None audio frame, continuing...")
                    continue
                try:
                    self.audio_track.add_frame(frame)
                except Exception as frame_error:
                    logger.error(f"Simli: Error processing audio frame: {frame_error}")
                    continue
            except Exception as e:
                logger.error(f"Simli: Audio processing error: {e}")
                if not self.run or self._stopping:
                    break
                await asyncio.sleep(0.1)
                continue

    async def sendSilence(self, duration: float = 0.1875):
        """Send silence to bootstrap the connection"""
        await self.simli_client.ready.wait()
        await self.simli_client.sendSilence(duration)

    async def _speech_timeout_handler(self):
        try:
            await asyncio.sleep(0.2)
            if self._is_speaking:
                await self.simli_client.clearBuffer()
                self._is_speaking = False
        except asyncio.CancelledError:
            pass
        except Exception as e:
            logger.error(f"Error in speech timeout handler: {e}")

    async def handle_audio_input(self, audio_data: bytes):
        if not self.run or self._stopping:
            return
        if self.simli_client.ready.is_set():
            try:
                if len(audio_data) % 2 != 0:
                    audio_data = audio_data + b"\x00"

                audio_array = np.frombuffer(audio_data, dtype=np.int16)
                input_frame = AudioFrame.from_ndarray(
                    audio_array.reshape(1, -1), format="s16", layout="mono"
                )
                input_frame.sample_rate = 24000

                resampled_frames = simli_input_resampler.resample(input_frame)
                for frame in resampled_frames:
                    resampled_data = frame.to_ndarray().tobytes()

                    await self.simli_client.send(resampled_data)

                    self._last_audio_time = time.time()

            except Exception as e:
                logger.error(f"Error processing/sending audio data: {e}")
        else:
            logger.error(
                f"Simli: Cannot send audio - ws available: {self.simli_client is not None}, ready: {self.simli_client.ready.is_set()}"
            )

    async def aclose(self):
        if self._stopping:
            return
        self._stopping = True
        self.run = False

        if self._keep_alive_task and not self._keep_alive_task.done():
            self._keep_alive_task.cancel()

        try:
            await self.simli_client.stop()
        except Exception:
            pass

    async def _keep_alive_loop(self):
        """Send periodic keep-alive audio to maintain Simli session"""

        while self.run and not self._stopping:
            try:
                current_time = time.time()
                if current_time - self._last_audio_time > 5.0:
                    if self.simli_client.ready.is_set():
                        try:
                            self._last_audio_time = current_time
                            await self.simli_client.sendSilence()

                        except Exception as e:
                            print(f"Simli: Keep-alive send failed: {e}")

                await asyncio.sleep(3.0)

            except Exception:
                if not self.run or self._stopping:
                    break
                await asyncio.sleep(1.0)

Initialize the Simli Avatar plugin.

Args

config : SimliConfig
The configuration for the Simli avatar.
simli_url : str
The Simli API URL. Defaults to "https://api.simli.ai".
is_trinity_avatar : bool
Specify if the face id in the simli config is a trinity avatar to ensure synchronization

Methods

async def aclose(self)
Expand source code
async def aclose(self):
    if self._stopping:
        return
    self._stopping = True
    self.run = False

    if self._keep_alive_task and not self._keep_alive_task.done():
        self._keep_alive_task.cancel()

    try:
        await self.simli_client.stop()
    except Exception:
        pass
async def connect(self)
Expand source code
async def connect(self):
    loop = asyncio.get_event_loop()
    await self._initialize_connection()
    self.audio_track = SimliAudioTrack(loop, self.simli_client)
    self.video_track = SimliVideoTrack(self._is_trinity_avatar)
    if self._stream_start_time is None:
        self._stream_start_time = time.time()

    self._last_audio_time = time.time()
    self._keep_alive_task = asyncio.create_task(self._keep_alive_loop())
async def handle_audio_input(self, audio_data: bytes)
Expand source code
async def handle_audio_input(self, audio_data: bytes):
    if not self.run or self._stopping:
        return
    if self.simli_client.ready.is_set():
        try:
            if len(audio_data) % 2 != 0:
                audio_data = audio_data + b"\x00"

            audio_array = np.frombuffer(audio_data, dtype=np.int16)
            input_frame = AudioFrame.from_ndarray(
                audio_array.reshape(1, -1), format="s16", layout="mono"
            )
            input_frame.sample_rate = 24000

            resampled_frames = simli_input_resampler.resample(input_frame)
            for frame in resampled_frames:
                resampled_data = frame.to_ndarray().tobytes()

                await self.simli_client.send(resampled_data)

                self._last_audio_time = time.time()

        except Exception as e:
            logger.error(f"Error processing/sending audio data: {e}")
    else:
        logger.error(
            f"Simli: Cannot send audio - ws available: {self.simli_client is not None}, ready: {self.simli_client.ready.is_set()}"
        )
async def mark_silent(self)
Expand source code
async def mark_silent(self):
    self._avatar_speaking = False
async def mark_speaking(self)
Expand source code
async def mark_speaking(self):
    self._avatar_speaking = True
async def sendSilence(self, duration: float = 0.1875)
Expand source code
async def sendSilence(self, duration: float = 0.1875):
    """Send silence to bootstrap the connection"""
    await self.simli_client.ready.wait()
    await self.simli_client.sendSilence(duration)

Send silence to bootstrap the connection

class SimliConfig (apiKey: str,
faceId: str,
syncAudio: bool = True,
handleSilence: bool = True,
maxSessionLength: int = 600,
maxIdleTime: int = 30,
model: simli.simli.SimliModels = SimliModels.fasttalk)
Expand source code
@dataclass
class SimliConfig:
    """
    Args:
        apiKey (str): Simli API Key
        faceId (str): Simli Face ID. If using Trinity, you need to specify "faceId/emotionId" in the faceId field to use a different emotion than the default
        handleSilence (bool): Simli server keeps sending silent video when the input buffer is fully depleted. Turning this off makes the video freeze when you don't send in anything
        maxSessionLength (int):
            Absolute maximum session duration, avatar will disconnect after this time
            even if it's speaking.
        maxIdleTime (int):
            Maximum duration the avatar is not speaking for before the avatar disconnects.
    """

    apiKey: str
    faceId: str
    syncAudio: bool = True
    handleSilence: bool = True
    maxSessionLength: int = 600
    maxIdleTime: int = 30
    model: SimliModels = SimliModels.fasttalk

Args

apiKey : str
Simli API Key
faceId : str
Simli Face ID. If using Trinity, you need to specify "faceId/emotionId" in the faceId field to use a different emotion than the default
handleSilence : bool
Simli server keeps sending silent video when the input buffer is fully depleted. Turning this off makes the video freeze when you don't send in anything

maxSessionLength (int): Absolute maximum session duration, avatar will disconnect after this time even if it's speaking. maxIdleTime (int): Maximum duration the avatar is not speaking for before the avatar disconnects.

Instance variables

var apiKey : str
var faceId : str
var handleSilence : bool
var maxIdleTime : int
var maxSessionLength : int
var model : simli.simli.SimliModels
var syncAudio : bool