-
Notifications
You must be signed in to change notification settings - Fork 3.3k
fix(plugins/bithuman): make Pillow optional + close AvatarRunner in aclose() #6191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 FormData object reuse across retries may cause issues In (Refers to lines 589-612) Was this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed this is a real issue, and as you note it's pre-existing (the |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,22 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import base64 | ||
| import io | ||
| import os | ||
| import sys | ||
| from collections.abc import AsyncGenerator, AsyncIterator | ||
| from typing import TYPE_CHECKING, Literal | ||
| from urllib.parse import parse_qs, urlparse | ||
|
|
||
| import aiohttp | ||
| import cv2 | ||
| import numpy as np | ||
| from loguru import logger as _logger | ||
|
Check failure on line 15 in livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
|
||
| from PIL import Image | ||
| try: | ||
| from PIL import Image | ||
| except ModuleNotFoundError: # Pillow only needed for cloud-mode avatar_image | ||
| Image = None | ||
|
|
||
| from livekit import api, rtc | ||
| from livekit.agents import ( | ||
|
|
@@ -196,10 +199,15 @@ | |
| raise BitHumanException("BITHUMAN_API_URL are required for cloud mode") | ||
|
|
||
| self._avatar_image: Image.Image | str | None = None | ||
| if isinstance(avatar_image, Image.Image): | ||
| if Image is not None and isinstance(avatar_image, Image.Image): | ||
| self._avatar_image = avatar_image | ||
| elif isinstance(avatar_image, str): | ||
| if os.path.exists(avatar_image): | ||
| if Image is None: | ||
| raise BitHumanException( | ||
| "Pillow is required to load an avatar image from a file path. " | ||
| "Install it with: pip install Pillow" | ||
| ) | ||
| self._avatar_image = Image.open(avatar_image) | ||
| elif avatar_image.startswith(("http://", "https://")): | ||
| self._avatar_image = avatar_image | ||
|
|
@@ -423,7 +431,7 @@ | |
| } | ||
|
|
||
| # Handle avatar image - convert to base64 for JSON serialization | ||
| if isinstance(self._avatar_image, Image.Image): | ||
| if Image is not None and isinstance(self._avatar_image, Image.Image): | ||
| img_byte_arr = io.BytesIO() | ||
| self._avatar_image.save(img_byte_arr, format="JPEG", quality=95) | ||
| img_byte_arr.seek(0) | ||
|
|
@@ -502,7 +510,7 @@ | |
| form_data.add_field("async_mode", "true" if async_mode else "false") | ||
|
|
||
| # Handle avatar image - send as file upload or URL | ||
| if isinstance(self._avatar_image, Image.Image): | ||
| if Image is not None and isinstance(self._avatar_image, Image.Image): | ||
| # Convert PIL Image to bytes and upload as file | ||
| img_byte_arr = io.BytesIO() | ||
| self._avatar_image.save(img_byte_arr, format="JPEG", quality=95) | ||
|
|
@@ -624,6 +632,13 @@ | |
|
|
||
| async def aclose(self) -> None: | ||
| await super().aclose() | ||
| # Tear down the AvatarRunner too: the base aclose() only removes the | ||
| # room participant, leaving the runner's read/forward tasks, the AV | ||
| # synchronizer, and the rust audio/video sources open — a native leak | ||
| # that accumulates per session/reconnect. | ||
| if self._avatar_runner is not None: | ||
| await self._avatar_runner.aclose() | ||
| self._avatar_runner = None | ||
|
Comment on lines
+639
to
+641
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Pre-existing naming inconsistency in AvatarRunner could cause AttributeError during partial-init cleanup The new cleanup code at lines 639-641 calls Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| if self._mode == "local" and utils.is_given(self._runtime) and self._runtime is not None: | ||
| self._runtime.cleanup() | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.