Skip to content

Commit 32b0553

Browse files
Fix soundcard import. Add Twillio TURN server.
2 parents 3633560 + d684e0b commit 32b0553

File tree

6 files changed

+52
-22
lines changed

6 files changed

+52
-22
lines changed

pyproject.toml

+6-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
license = "MIT"
55
name = "pyrobbot"
66
readme = "README.md"
7-
version = "0.7.0"
7+
version = "0.7.1"
88

99
[build-system]
1010
build-backend = "poetry.core.masonry.api"
@@ -24,7 +24,7 @@
2424
# Other dependencies
2525
loguru = "^0.7.2"
2626
numpy = "^1.26.1"
27-
openai = "^1.12.0"
27+
openai = "^1.13.3"
2828
pandas = "^2.2.0"
2929
pillow = "^10.2.0"
3030
pydantic = "^2.6.1"
@@ -34,18 +34,19 @@
3434
audio-recorder-streamlit = "^0.0.8"
3535
beautifulsoup4 = "^4.12.3"
3636
chime = "^0.7.0"
37-
duckduckgo-search = {git = "https://github.com/deedy5/duckduckgo_search"}
37+
duckduckgo-search = {url = "https://github.com/deedy5/duckduckgo_search/archive/refs/tags/v5.0b1.tar.gz"}
3838
gtts = "^2.5.1"
3939
httpx = "^0.26.0"
4040
ipinfo = "^5.0.1"
4141
pydub = "^0.25.1"
4242
pygame = "^2.5.2"
43-
setuptools = "^68.2.2" # Needed by webrtcvad-wheels
43+
setuptools = "^68.2.2" # Needed by webrtcvad-wheels
4444
sounddevice = "^0.4.6"
4545
soundfile = "^0.12.1"
4646
speechrecognition = "^3.10.0"
4747
streamlit-mic-recorder = "^0.0.4"
4848
streamlit-webrtc = "^0.47.1"
49+
twilio = "^9.0.0"
4950
tzlocal = "^5.2"
5051
unidecode = "^1.3.7"
5152
webrtcvad-wheels = "^2.0.11.post1"
@@ -58,7 +59,7 @@
5859
flakeheaven = "^3.3.0"
5960
isort = "^5.13.2"
6061
pydoclint = "^0.4.0"
61-
ruff = "^0.2.1"
62+
ruff = "^0.3.0"
6263

6364
[tool.poetry.group.test.dependencies]
6465
pytest = "^8.0.0"

pyrobbot/app/app_utils.py

+20
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import contextlib
44
import datetime
5+
import os
56
import queue
67
import threading
78
from typing import TYPE_CHECKING
@@ -11,6 +12,7 @@
1112
from PIL import Image
1213
from pydub import AudioSegment
1314
from streamlit.runtime.scriptrunner import add_script_run_ctx
15+
from twilio.rest import Client as TwilioClient
1416

1517
from pyrobbot import GeneralDefinitions
1618
from pyrobbot.chat import AssistantResponseChunk
@@ -149,6 +151,24 @@ def stream_text_and_audio_reply(self):
149151
return {"text": full_response, "audio": full_audio_fpath}
150152

151153

154+
@st.cache_data
155+
def get_ice_servers():
156+
"""Use Twilio's TURN server as recommended by the streamlit-webrtc developers."""
157+
try:
158+
account_sid = os.environ["TWILIO_ACCOUNT_SID"]
159+
auth_token = os.environ["TWILIO_AUTH_TOKEN"]
160+
except KeyError:
161+
logger.warning(
162+
"Twilio credentials are not set. Cannot use their TURN servers. "
163+
"Falling back to a free STUN server from Google."
164+
)
165+
return [{"urls": ["stun:stun.l.google.com:19302"]}]
166+
167+
client = TwilioClient(account_sid, auth_token)
168+
token = client.tokens.create()
169+
return token.ice_servers
170+
171+
152172
def filter_page_info_from_queue(app_page: "AppPage", the_queue: queue.Queue):
153173
"""Filter `app_page`'s data from `queue` inplace. Return queue of items in `app_page`.
154174

pyrobbot/app/multipage.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@
1818
from pydantic import ValidationError
1919
from pydub import AudioSegment
2020
from streamlit.runtime.scriptrunner import add_script_run_ctx
21-
from streamlit_webrtc import RTCConfiguration, WebRtcMode
21+
from streamlit_webrtc import WebRtcMode
2222

2323
from pyrobbot import GeneralDefinitions
2424
from pyrobbot.chat_configs import VoiceChatConfigs
2525
from pyrobbot.general_utils import trim_beginning
2626
from pyrobbot.openai_utils import OpenAiClientWrapper
2727

2828
from .app_page_templates import AppPage, ChatBotPage, _RecoveredChat
29-
from .app_utils import WebAppChat, filter_page_info_from_queue, get_avatar_images
29+
from .app_utils import (
30+
WebAppChat,
31+
filter_page_info_from_queue,
32+
get_avatar_images,
33+
get_ice_servers,
34+
)
3035

3136
incoming_frame_queue = queue.Queue()
3237
possible_speech_chunks_queue = queue.Queue()
@@ -308,6 +313,11 @@ def __init__(self, **kwargs) -> None:
308313
self.text_prompt_queue = text_prompt_queue
309314
self.reply_ongoing = reply_ongoing
310315

316+
@property
317+
def ice_servers(self):
318+
"""Return the ICE servers for WebRTC."""
319+
return get_ice_servers()
320+
311321
@property
312322
def continuous_audio_input_engine_is_running(self):
313323
"""Return whether the continuous audio input engine is running."""
@@ -318,12 +328,7 @@ def continuous_audio_input_engine_is_running(self):
318328
)
319329

320330
def render_continuous_audio_input_widget(self):
321-
"""Render the continuous audio input widget."""
322-
# Definitions related to webrtc_streamer
323-
rtc_configuration = RTCConfiguration(
324-
{"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]}
325-
)
326-
331+
"""Render the continuous audio input widget using webrtc_streamer."""
327332
try:
328333
selected_page = self.selected_page
329334
except StopIteration:
@@ -377,7 +382,7 @@ def audio_frame_callback(frame):
377382
self.stream_audio_context = streamlit_webrtc.component.webrtc_streamer(
378383
key="sendonly-audio",
379384
mode=WebRtcMode.SENDONLY,
380-
rtc_configuration=rtc_configuration,
385+
rtc_configuration={"iceServers": self.ice_servers},
381386
media_stream_constraints={"audio": True, "video": False},
382387
desired_playing_state=True,
383388
audio_frame_callback=audio_frame_callback,

pyrobbot/voice_chat.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
try:
2626
import sounddevice as sd
2727
except OSError as error:
28-
logger.error(error)
28+
logger.exception(error)
2929
logger.error(
3030
"Can't use module `sounddevice`. Please check your system's PortAudio install."
3131
)
@@ -516,8 +516,8 @@ def _assistant_still_replying(self):
516516
def _check_needed_imports():
517517
"""Check if the needed modules are available."""
518518
if not _sounddevice_imported:
519-
raise ImportError(
520-
"Module `sounddevice`, needed for audio recording, is not available."
519+
logger.error(
520+
"Module `sounddevice`, needed for local audio recording, is not available."
521521
)
522522

523523
if not _pydub_imported:

tests/conftest.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,15 @@ def _mock_input(*args, **kwargs): # noqa: ARG001
150150
)
151151

152152

153-
@pytest.fixture(params=ChatOptions.get_allowed_values("model")[:3])
153+
@pytest.fixture(params=ChatOptions.get_allowed_values("model")[:2])
154154
def llm_model(request):
155155
return request.param
156156

157157

158-
@pytest.fixture(params=ChatOptions.get_allowed_values("context_model")[:3])
158+
context_model_values = ChatOptions.get_allowed_values("context_model")
159+
160+
161+
@pytest.fixture(params=[context_model_values[0], context_model_values[2]])
159162
def context_model(request):
160163
return request.param
161164

tests/unit/test_voice_chat.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
from pyrobbot.voice_chat import VoiceChat
1111

1212

13-
def test_cannot_instanciate_assistant_if_soundcard_not_imported(mocker):
13+
def test_soundcard_import_check(mocker, caplog):
1414
"""Test that the voice chat cannot be instantiated if soundcard is not imported."""
1515
mocker.patch("pyrobbot.voice_chat._sounddevice_imported", False)
16-
with pytest.raises(ImportError, match="Module `sounddevice`"):
17-
VoiceChat(configs=VoiceChatConfigs())
16+
_ = VoiceChat(configs=VoiceChatConfigs())
17+
msg = "Module `sounddevice`, needed for local audio recording, is not available."
18+
assert msg in caplog.text
1819

1920

2021
@pytest.mark.parametrize("param_name", ["sample_rate", "frame_duration"])

0 commit comments

Comments
 (0)