From 95fc8026077d387315784348e1810a904f39ceba Mon Sep 17 00:00:00 2001 From: Moishe Lettvin Date: Fri, 26 Jan 2024 09:15:29 -0500 Subject: [PATCH 1/3] Speaking / waiting images --- src/dailyai/queue_aggregators.py | 33 ++++- src/dailyai/services/ai_services.py | 8 +- src/samples/foundational/06a-image-sync.py | 134 +++++++++++++++++++++ src/samples/foundational/speaking.png | Bin 0 -> 33854 bytes src/samples/foundational/waiting.png | Bin 0 -> 30719 bytes 5 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 src/samples/foundational/06a-image-sync.py create mode 100644 src/samples/foundational/speaking.png create mode 100644 src/samples/foundational/waiting.png diff --git a/src/dailyai/queue_aggregators.py b/src/dailyai/queue_aggregators.py index e2e5ff7fd..3b0ffbc7e 100644 --- a/src/dailyai/queue_aggregators.py +++ b/src/dailyai/queue_aggregators.py @@ -1,6 +1,6 @@ import asyncio -from dailyai.queue_frame import LLMMessagesQueueFrame, QueueFrame, TextQueueFrame +from dailyai.queue_frame import LLMMessagesQueueFrame, QueueFrame, TextQueueFrame, TranscriptionQueueFrame from dailyai.services.ai_services import AIService from typing import AsyncGenerator, List @@ -32,16 +32,24 @@ class LLMContextAggregator(AIService): messages: list[dict], role: str, bot_participant_id=None, - complete_sentences=True): + complete_sentences=True, + pass_through=False): self.messages = messages self.bot_participant_id = bot_participant_id self.role = role self.sentence = "" self.complete_sentences = complete_sentences + self.pass_through = pass_through async def process_frame(self, frame: QueueFrame) -> AsyncGenerator[QueueFrame, None]: # TODO: split up transcription by participant if isinstance(frame, TextQueueFrame): + + # Ignore transcription frames from the bot + if isinstance(frame, TranscriptionQueueFrame): + if frame.participantId == self.bot_participant_id: + return + if self.complete_sentences: self.sentence += frame.text if self.sentence.endswith((".", "?", "!")): @@ -52,4 +60,23 @@ class LLMContextAggregator(AIService): self.messages.append({"role": self.role, "content": frame.text}) yield LLMMessagesQueueFrame(self.messages) - yield frame + if self.pass_through: + yield frame + else: + yield frame + +class LLMUserContextAggregator(LLMContextAggregator): + def __init__(self, + messages: list[dict], + bot_participant_id=None, + complete_sentences=True): + super().__init__(messages, "user", bot_participant_id, complete_sentences, pass_through=False) + + +class LLMAssistantContextAggregator(LLMContextAggregator): + def __init__( + self, messages: list[dict], bot_participant_id=None, complete_sentences=True + ): + super().__init__( + messages, "assistan", bot_participant_id, complete_sentences, pass_through=True + ) diff --git a/src/dailyai/services/ai_services.py b/src/dailyai/services/ai_services.py index 3c5dba3c4..263cf186e 100644 --- a/src/dailyai/services/ai_services.py +++ b/src/dailyai/services/ai_services.py @@ -86,9 +86,7 @@ class LLMService(AIService): pass async def process_frame(self, frame: QueueFrame) -> AsyncGenerator[QueueFrame, None]: - if isinstance(frame, ControlQueueFrame): - yield frame - elif isinstance(frame, LLMMessagesQueueFrame): + if isinstance(frame, LLMMessagesQueueFrame): async for text_chunk in self.run_llm_async(frame.messages): yield TextQueueFrame(text_chunk) @@ -111,11 +109,9 @@ class TTSService(AIService): yield bytes() async def process_frame(self, frame: QueueFrame) -> AsyncGenerator[QueueFrame, None]: - if isinstance(frame, ControlQueueFrame): + if not isinstance(frame, TextQueueFrame): yield frame return - elif not isinstance(frame, TextQueueFrame): - return text: str | None = None if not self.aggregate_sentences: diff --git a/src/samples/foundational/06a-image-sync.py b/src/samples/foundational/06a-image-sync.py new file mode 100644 index 000000000..47bb025de --- /dev/null +++ b/src/samples/foundational/06a-image-sync.py @@ -0,0 +1,134 @@ +import argparse +import asyncio +from typing import AsyncGenerator +import requests +import time +import urllib.parse + +from PIL import Image +from dailyai.queue_frame import ImageQueueFrame, QueueFrame + +from dailyai.services.daily_transport_service import DailyTransportService +from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService +from dailyai.services.ai_services import AIService +from dailyai.queue_aggregators import LLMAssistantContextAggregator, LLMUserContextAggregator +from dailyai.services.fal_ai_services import FalImageGenService + + +class ImageSyncAggregator(AIService): + def __init__(self, speaking_path:str, waiting_path:str): + self._speaking_image = Image.open(speaking_path) + self._speaking_image_bytes = self._speaking_image.tobytes() + + self._waiting_image = Image.open(waiting_path) + self._waiting_image_bytes = self._waiting_image.tobytes() + + async def process_frame(self, frame: QueueFrame) -> AsyncGenerator[QueueFrame, None]: + yield ImageQueueFrame(None, self._speaking_image_bytes) + yield frame + yield ImageQueueFrame(None, self._waiting_image_bytes) + +async def main(room_url: str, token): + global transport + global llm + global tts + + transport = DailyTransportService( + room_url, + token, + "Respond bot", + 5, + ) + transport.camera_enabled = True + transport.camera_width = 1024 + transport.camera_height = 1024 + transport.mic_enabled = True + transport.mic_sample_rate = 16000 + + llm = AzureLLMService() + tts = AzureTTSService() + img = FalImageGenService(image_size="1024x1024") + + async def get_images(): + get_speaking_task = asyncio.create_task( + img.run_image_gen("An image of a cat speaking") + ) + get_waiting_task = asyncio.create_task( + img.run_image_gen("An image of a cat waiting") + ) + + (speaking_data, waiting_data) = await asyncio.gather( + get_speaking_task, get_waiting_task + ) + + return speaking_data, waiting_data + + @transport.event_handler("on_first_other_participant_joined") + async def on_first_other_participant_joined(transport): + await tts.say("Hi, I'm listening!", transport.send_queue) + + async def handle_transcriptions(): + messages = [ + {"role": "system", "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way."}, + ] + + tma_in = LLMUserContextAggregator( + messages, transport.my_participant_id + ) + tma_out = LLMAssistantContextAggregator( + messages, transport.my_participant_id + ) + image_sync_aggregator = ImageSyncAggregator( + "/Users/moishe/src/daily-ai-sdk/src/samples/foundational/speaking.png", + "/Users/moishe/src/daily-ai-sdk/src/samples/foundational/waiting.png", + ) + await tts.run_to_queue( + transport.send_queue, + image_sync_aggregator.run( + tma_out.run( + llm.run( + tma_in.run( + transport.get_receive_frames() + ) + ) + ) + ) + ) + + transport.transcription_settings["extra"]["punctuate"] = True + await asyncio.gather(transport.run(), handle_transcriptions()) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Simple Daily Bot Sample") + parser.add_argument( + "-u", "--url", type=str, required=True, help="URL of the Daily room to join" + ) + parser.add_argument( + "-k", + "--apikey", + type=str, + required=True, + help="Daily API Key (needed to create token)", + ) + + args, unknown = parser.parse_known_args() + + # Create a meeting token for the given room with an expiration 1 hour in the future. + room_name: str = urllib.parse.urlparse(args.url).path[1:] + expiration: float = time.time() + 60 * 60 + + res: requests.Response = requests.post( + f"https://api.daily.co/v1/meeting-tokens", + headers={"Authorization": f"Bearer {args.apikey}"}, + json={ + "properties": {"room_name": room_name, "is_owner": True, "exp": expiration} + }, + ) + + if res.status_code != 200: + raise Exception(f"Failed to create meeting token: {res.status_code} {res.text}") + + token: str = res.json()["token"] + + asyncio.run(main(args.url, token)) diff --git a/src/samples/foundational/speaking.png b/src/samples/foundational/speaking.png new file mode 100644 index 0000000000000000000000000000000000000000..d7bab6fd7f7aa0f7003752e65c16171ef11787ef GIT binary patch literal 33854 zcmeFZhhJ0Mx-J}$DkuU9A__Ai)b0@8!1Km-J&M5Kh?tDuN26aM?xx2}z+m7Tr0 zE8Lk)*BQko?uhRp7ex1JQqv|AF{l{gHq^T=?&6pdLt6SX5kASVUG- zl1*4zRzzA>SOWA-KUo$iqI6V)x`04}9E2Yt&s;@!5a>ds%@adcLv1Zt3%I?Yxh4FC zm7u4+BcUe{*i#m`w6}6KXY;hTb8wOMlxP2|hAePRxGlua_E!~GTX}XvZC$npaAzwv zNkL&jVRnU!Y;0^`XG?2YJ!RE@nghSdv%hq8b(9qn^6>Bw^biw-JHv!TWMpK7ghho! zMFoHw0xn(-uI8Qs4lW#jck-`(l&xGWoNXLkZQu@UgnrFmz};Ns+1UvL{nzL3=XABP z{_l|-T>hCBFhLMX60$~zceG1{IAV`cl2M=`acHqGF(O)m)?`H%P zo_x`Ou;YG>*wVk2LHuhEyE`%A109z6)^dF1{b*_KYOZYVYNbFp1Omb$0-};nfI}cIDJvl?AS@wEc)$O@y8ok} z2hLVN5C5T;zmC$s_wsKw|5G1u=O=Kuox*=Q+kZdBU+w>?^-l+2A;Ql7x1Ij?G5YH+ zaE=r%0xkb_CKWDTS{OisKuREW<$F&&iPk5)>H-Gq6di|MP4*UU%q_j;d>p2BJEd4x znO7qRtt)CxaXbEFkZzvdyZHCao&0V#SLjj@wspgfJ*^9qQ)^3mr+)rR$i30E^*yHE zZ9hc}>s0$vt>S6f;d4xd!cyIoZArz!)Y)MG{_xt_6*i?n5D_s6#T7Q)2h;B*-WS9Z zuK&4t#!5sP=>A_dls?1+l6=s$;=B8I3qk>9pyZ$Of8OAq_w#3e{5cDsm@aTom#R3Zi6Tf5w-c?1KgI;?A~MgW$rX=gXabTM6;DSo{(EUtmZzq;-6X!} zJMRtZSD%)6_1He&@*b|8xditdXvATU+N62|O8kEp@Fb zxV1*yTE61>ib2C}f(u6e@N5r*AF68j4aXnADNpcwig+=&o5{kuiH$Z$qT=gn}WC)Mj!uy}B7o4Ur|hUy{q_I@BC&w!-7cYk--V_bMnrI* z@o`}BP2DzZqRzxGn8frgIn>NgGE**=}Zj^vBNUGQ_<7&rz$8aO=cPNYZ^2MlDb!?^#Ao9BIhuS?VOHv!zkeJh=O$*ZRj_uwbgx%#>v87EFUH2BAK?4a@CrvlyE#D=%y^gOc9+emVSp^3QU= zb0pOs!@m}qFht7dCoxY2VwAwa0lP1ARF#pJabKz?WigQY@rg~zN&4D28q1EK3`Tab z9P7Z@dU(_6yG?$^(C_r@AcG;cP7Y$=W-VCQ^C?x0H1PmKht#I*FiM9-OLDhqW8Y`~ zp&h4EVAC^N;0-3EI%n7#O#|D}9Qpec`1)9QJxqn=xZ`2NqC`nb=gahY8V^*LNtD9L z@>$yCc{lyCEWC1QLaKg68_k5<+`<)qORJ(NvLI%L*ExyK7kh03$9z&&|M`#Gz!xE-S6UOuzSyhBsN>1LYDcZ_G7*qCONwaka^ zn;k{L#eC%%O*K3*yuYoRN_+-CE0a*{(|)CX$;x$lC0T8nuoclfndzf7CJ#p)TQ3nL zhL+R`YdT*mv-_A$K4Zi7o8js6PpY!QNMA`J-Ac)GaowWaOY8eVy4#41vKBCpHP+}N4-hc;;yM;ls%jy*%!{qHF$l6QL{p}Vi zq4g6xx&%N@9d;dy2NINvx{RpCT@My#X3o8Yy(e>EgeQ|jnCorv$|d$aZ21{=@xgn8 zx)*}d!PPfdq0LpI-{hzum;9Z3P;thN#gd1wcM*(*^j@St^jixa+aBw6+gcPzCsf>c z)h=HXs!a?M^CJa~NK%B^DV31N2^1Fdzb$Vlu`q+zC@IxL z8%_aPa@=Lwiy&_}-+=S1btHZ-K-JB&034-&fN&!$yDj^rO@sLtrGE2+f~{@e%?T3_ zYCPM8<9;s@ge6->nok9OE#SNaYk=CYhu7jx)(DEvv~tYY@a%gOdTO@=MJ-di->>O{ z_R2`e(91_nud|}(4b-IpQM5KW&n<-}om$nes#y?#r~HQPDPgbG?Y^<%t>CIS07S$z z^VCtBLF36_vlKfOd44C|{?g%t&)f+Ksbep*BOUzKjDT_}ows$}3aI4rGeFdx9t)l= zKUC~YV#kPU1*2DqS6b9g(Q{NTVqzTB2HWE`-P}44&At%Si1(!$+@N60cA>~u|DVV8 zc2;ViQWg76y8ITSz5P#&wOClvxBVqe)CK?*yWK>tAgsx0Mdor@&KU!>)8fqNlH01=Q#hNUS(+k|SC715kah}sblx6t z)(%$RxJToyTUx}7ohIuVwMu(-ItD0#Fo^H$94p>)C|z48L4a3B z(r55Fl?L5*7N7Nwgoa{3l`76qLf);mL@(9ZJVW7V{I`Qs0;Y;9d^wyyk&G4;gvzg* zAEGdRxEWTQ`H;Qcc1907Fb>&WXeVG?r1{b=N`5gmY)G?FLGx5$ox1`~@&h^t+V>Vr zJnfIoI;HjGD$2S|Q{D`kQw2E_A9J7HRx)!Z=0SSC;K=G^?Q;Fnv$?((l^!_fUR7%% zJN6Ja5AmGs@M2{;o>FdGN$$?vc%#Tc6X$tCP*iQq@bqRL%@eLPVfN<7_do4n6a%h! zMbsf}?6q8-N3D|TPX>ujez|6u8s*E4+2wZ+T*;ed7q9boGB?6sEzfD?2WLa$?Z zWGf&gMKDKmo>B$412c1t*IJvs6$%PcAlAeU4lE5L{3xzmzsQM&jxb63N@cuSVa)vf zDQ#wjj=KRkIOig)q+%YAFpVd!)-*0mSdK$kkWX z_&P?JC->2FQF5Cctj8VnnMdF24=3g1-|QWv3C_~aeuLcFK^4z(+XDj>yRCbd^$-<%D2ozvrGfi)L(XTI)L zKGTNP*2q`dotX!r0%s0zLe9-lsI=6jUfH#x@5_%%^01w1%${@1Q^|Z<+&}RxyY|{E z&OCRMss{CqxKg_5V6D_Vl)hwr3&RsF7Zx{1J+D&HSWyuX`;nelQXT-C(Jn~&?#j2d zllzM$7B8yLc9>6(_;#DApDo{gqRPt|$YT7I>@eSTwEyWDmp$a}Z@^!X{`UJ9if&>4 z@&hE|XJc6zCL!N>V8Ls(AfNk1nuEkS!2uZn_J@|tvI~>NIwFm-s_}e-o;1pDp{({o zOlPNdbVx*`GuP~t>wvZ*aJVYcG@SF>{E+)U4~r^r9-#g&$jMD?fUU7FzDln6xA{f%)JCUclZ`-n8lV@gT*gE#o7^dex=^7Gph3+SpDomWNt4%d4kbC+q8# zOIxms2!eqIn69;AYaVxbIz{Nu6@gu&gGfFL{MO@F*$!S5$$eArh2??>oDyvz1Z@#ph`?kn=^Ut2}9@wOQA>T8r73Z*`wbs9t z@iv91@t6Ys$k5jw37_iaBM~w;&kSEi3_qoqT!q)KBMhX=95g_6v zDIIGrj4@7|5R)WAm59G~PwN=yu=NG_2*jSP$S)>akR5goU&`3e=Gn(=udTR$qH&^A zSNvT7>B)Ib$&sLpH-}`!_2FBCF|#V0CYEEi&Ve~hh?|P2?^MsXUk4l~1*|}~m7AEP z5}0+N#UhGKUU7Iqn_@@eYJz*ca(&aQOkHx&^?q7du#bYWIsf}DzgfwRqPx9jAq=985C8?9cP%MP{ol^wZr ze0@c#Mf|+t>0HYYZBRsB{bBu9MhYmBWz7|lUnnsboO#?0vsO87`+TpEm8)4fuiu)? ze0b_K{0&*0nLbWZZXgyZA*a5_ib zUdGnhQUGpA^+6{s-H~-rUU?QtGgEseRc{MFbWe(j7U(*VfUHe$qQi9eHac>Si!Jh|Q=C)ZCcUw(Ie^A)qF|5=djmY-Iz0`?J@83~+6_spV{37lt39Mut;_eIEHk4GTYRRech^**7JX^;By66nOk)f_6k6p)E+{B%UbJ39h3=+;f)cKFBu&u}5sO{xC>8d5 zo$uOX4Q=Jmhz*)MwvZXnOx>Q2RBxg*?H{e%PI{J=&OcqYR$ObXXV-H0T}u7h+oafG zCR?PLY(@`L41yOK?*3#c&`Vi~J!j_LNn^)~j?6;57RJys!=-BeDwKhDj$L$3d^lZ+ zhgK^qy&0nS?z=o?UUsY;9ST$Saxrn5HFW1904yjz`VFwzP@RZhbHPHPMe;RX3bz?nVn}9we{GGesq= zb^p$fR-|cG)0lEASh=Wv%If8d2wZyl9#3aT7R1NjDwFT> zc&4Ri+VTokldqM)+$!zL}mS_fTFlpfY%+ zNH>M2^tYnjfmV{EcXe&5hM+W&w`dI4Etkz+CjZ1xK`E_%qQ)HD?*dNHPWPRXgNZU- z#c3j%jw%ZWC+~*m1te@1Xi+*l_2`{W;j$vOU%f=TDqZW_NGpuP3bf;H{YEa_g`C5q zslWULzlfHSk`ffXQrMTiSYb)2gnw4X6J!H8v3Xo8(j?D_SK53wT{6dUr}A6TvsnCM zw5w|OQl%363V(l6A8{Cq{&ueto`g+4g9)ZM`60=*5H<;uBUd|1nvwF8T_GQ&FQ=x5 z^SdBFamg2;4*L~nQw(+rGF;W9vN9vCK3jbmS#&Vs`ONq&*(C;eiU*hG34GISK331g zXaYNo3Ptdje2-y+_%D&S9E-Jx#rQt$4J}Vk!T&i)2HFnL6uA8}E#qbU z%}P^FPi?Jqa(nJ=`I!*`OSQ zTW1?#e5i^+7iz=JV#`^$fm{Ac-oR~5oyepbnqnEvA#1~DnNyy0fvAdgiJ%1nIx+_@ zucFX;%r=)yf8Rk(QLza0oImIhDh%b_Zf7}Qf zEANt8X5BFY)4KQ#0K}GNlq1!+EYM0x-B#(ol*g`Uo@58D zV(ol47OS62sgFP13wVXQ1#NPj1*Co@z2xVcGA-)5VykxZQ0D@sH~o9BAF_PJw|&n8 zU_9T=WCM-i7C64jQ1Q^n`=oZx^;kKDrfYNTiDbrJ5sWfy&l@0Nrek7xN8hwUZLLfwtYWN&JhqNRhf-L+f5hB zDy?hawZu~@(csC;OgX=h24ncIVK}i1rLhvZkMXnNLJq9L3Q%Q5lN^)UTptNd!6An%0D?kizU?7;5pJY?W4_& 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 Date: Fri, 26 Jan 2024 09:54:51 -0500 Subject: [PATCH 2/3] little more cleanup --- src/dailyai/conversation_wrappers.py | 6 +-- src/dailyai/queue_aggregators.py | 48 +++++++++++--------- src/samples/foundational/07-interruptible.py | 1 + 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/dailyai/conversation_wrappers.py b/src/dailyai/conversation_wrappers.py index bc7dd902d..bb83f1272 100644 --- a/src/dailyai/conversation_wrappers.py +++ b/src/dailyai/conversation_wrappers.py @@ -2,7 +2,7 @@ import asyncio import copy import functools from typing import AsyncGenerator, Awaitable, Callable -from dailyai.queue_aggregators import LLMContextAggregator +from dailyai.queue_aggregators import LLMAssistantContextAggregator, LLMContextAggregator, LLMUserContextAggregator from dailyai.queue_frame import EndStreamQueueFrame, QueueFrame, TranscriptionQueueFrame @@ -17,8 +17,8 @@ class InterruptibleConversationWrapper: interrupt: Callable[[], None], my_participant_id: str | None, llm_messages: list[dict[str, str]], - llm_context_aggregator_in=LLMContextAggregator, - llm_context_aggregator_out=LLMContextAggregator, + llm_context_aggregator_in=LLMUserContextAggregator, + llm_context_aggregator_out=LLMAssistantContextAggregator, delay_before_speech_seconds: float = 1.0, ): self._frame_generator: Callable[[], AsyncGenerator[QueueFrame, None]] = frame_generator diff --git a/src/dailyai/queue_aggregators.py b/src/dailyai/queue_aggregators.py index 3b0ffbc7e..545c86728 100644 --- a/src/dailyai/queue_aggregators.py +++ b/src/dailyai/queue_aggregators.py @@ -33,7 +33,7 @@ class LLMContextAggregator(AIService): role: str, bot_participant_id=None, complete_sentences=True, - pass_through=False): + pass_through=True): self.messages = messages self.bot_participant_id = bot_participant_id self.role = role @@ -42,28 +42,32 @@ class LLMContextAggregator(AIService): self.pass_through = pass_through async def process_frame(self, frame: QueueFrame) -> AsyncGenerator[QueueFrame, None]: - # TODO: split up transcription by participant - if isinstance(frame, TextQueueFrame): - - # Ignore transcription frames from the bot - if isinstance(frame, TranscriptionQueueFrame): - if frame.participantId == self.bot_participant_id: - return - - if self.complete_sentences: - self.sentence += frame.text - if self.sentence.endswith((".", "?", "!")): - self.messages.append({"role": self.role, "content": self.sentence}) - self.sentence = "" - yield LLMMessagesQueueFrame(self.messages) - else: - self.messages.append({"role": self.role, "content": frame.text}) - yield LLMMessagesQueueFrame(self.messages) - - if self.pass_through: - yield frame - else: + # We don't do anything with non-text frames, pass it along to next in the pipeline. + if not isinstance(frame, TextQueueFrame): yield frame + return + + # The common case for "pass through" is receiving frames from the LLM that we'll + # use to update the "assistant" LLM messages, but also passing the text frames + # along to a TTS service to be spoken to the user. + if self.pass_through: + yield frame + + # Ignore transcription frames from the bot + if isinstance(frame, TranscriptionQueueFrame): + if frame.participantId == self.bot_participant_id: + return + + # TODO: split up transcription by participant + if self.complete_sentences: + self.sentence += frame.text # type: ignore -- the linter thinks this isn't a TextQueueFrame, even though we check it above + if self.sentence.endswith((".", "?", "!")): + self.messages.append({"role": self.role, "content": self.sentence}) + self.sentence = "" + yield LLMMessagesQueueFrame(self.messages) + else: + self.messages.append({"role": self.role, "content": frame.text}) # type: ignore -- the linter thinks this isn't a TextQueueFrame, even though we check it above + yield LLMMessagesQueueFrame(self.messages) class LLMUserContextAggregator(LLMContextAggregator): def __init__(self, diff --git a/src/samples/foundational/07-interruptible.py b/src/samples/foundational/07-interruptible.py index cd18804af..927a5670f 100644 --- a/src/samples/foundational/07-interruptible.py +++ b/src/samples/foundational/07-interruptible.py @@ -25,6 +25,7 @@ async def main(room_url: str, token): transport.mic_enabled = True transport.mic_sample_rate = 16000 transport.camera_enabled = False + transport.start_transcription = True llm = AzureLLMService() tts = ElevenLabsTTSService(voice_id="ErXwobaYiN019PkySvjV") From ead655fe238fca6462f0b3b73052dcadd08e2d58 Mon Sep 17 00:00:00 2001 From: Moishe Lettvin Date: Fri, 26 Jan 2024 10:07:16 -0500 Subject: [PATCH 3/3] some more fixup --- src/dailyai/conversation_wrappers.py | 4 ++-- src/dailyai/queue_aggregators.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dailyai/conversation_wrappers.py b/src/dailyai/conversation_wrappers.py index bb83f1272..7f688477c 100644 --- a/src/dailyai/conversation_wrappers.py +++ b/src/dailyai/conversation_wrappers.py @@ -43,10 +43,10 @@ class InterruptibleConversationWrapper: async def speak_after_delay(self, user_speech, messages): await asyncio.sleep(self._delay_before_speech_seconds) tma_in = self._llm_context_aggregator_in( - messages, "user", self._my_participant_id, False + messages, self._my_participant_id, complete_sentences=False ) tma_out = self._llm_context_aggregator_out( - messages, "assistant", self._my_participant_id + messages, self._my_participant_id ) await self._runner(user_speech, tma_in, tma_out) diff --git a/src/dailyai/queue_aggregators.py b/src/dailyai/queue_aggregators.py index 545c86728..55461e29c 100644 --- a/src/dailyai/queue_aggregators.py +++ b/src/dailyai/queue_aggregators.py @@ -47,17 +47,17 @@ class LLMContextAggregator(AIService): yield frame return + # Ignore transcription frames from the bot + if isinstance(frame, TranscriptionQueueFrame): + if frame.participantId == self.bot_participant_id: + return + # The common case for "pass through" is receiving frames from the LLM that we'll # use to update the "assistant" LLM messages, but also passing the text frames # along to a TTS service to be spoken to the user. if self.pass_through: yield frame - # Ignore transcription frames from the bot - if isinstance(frame, TranscriptionQueueFrame): - if frame.participantId == self.bot_participant_id: - return - # TODO: split up transcription by participant if self.complete_sentences: self.sentence += frame.text # type: ignore -- the linter thinks this isn't a TextQueueFrame, even though we check it above