Compare commits
1119 Commits
hush/rtviS
...
jpt/runner
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1f056aa7 | ||
|
|
2be615066c | ||
|
|
1bb821a07d | ||
|
|
d8bcb81f35 | ||
|
|
3ce0ab8c6d | ||
|
|
097d786431 | ||
|
|
662f04879c | ||
|
|
7a69f57e11 | ||
|
|
5b7b4efdc9 | ||
|
|
cfa26524ca | ||
|
|
3d4ab7158d | ||
|
|
26d1ca3c98 | ||
|
|
083b32887e | ||
|
|
3391929127 | ||
|
|
ebf9bc2741 | ||
|
|
f5edde42f6 | ||
|
|
37bb7ef926 | ||
|
|
a63d1530a4 | ||
|
|
960bc9df5b | ||
|
|
e2a153ee01 | ||
|
|
300f19ad23 | ||
|
|
7955080da2 | ||
|
|
994e82c1ef | ||
|
|
b07b947352 | ||
|
|
a6527c3856 | ||
|
|
0e6874b605 | ||
|
|
9ba172c49f | ||
|
|
f710c94b6e | ||
|
|
6e3a0a2d5d | ||
|
|
9530b8b842 | ||
|
|
26c937af87 | ||
|
|
976f6168f0 | ||
|
|
0be64e0fd9 | ||
|
|
7d527c3a6b | ||
|
|
c6f6930c27 | ||
|
|
c33dfe8309 | ||
|
|
769cd1ef06 | ||
|
|
6d72f60571 | ||
|
|
e8d0712ac1 | ||
|
|
88b2c817ac | ||
|
|
f8f6c9918d | ||
|
|
8ee608bbfe | ||
|
|
fad2ba4570 | ||
|
|
f609f7eb53 | ||
|
|
ea09813a2b | ||
|
|
53abfc27a7 | ||
|
|
9c72e96a2c | ||
|
|
f66c67c4ab | ||
|
|
b623face03 | ||
|
|
698d60f3ae | ||
|
|
c9717a23a5 | ||
|
|
d981ce6e56 | ||
|
|
1bbd3bd8ab | ||
|
|
a20915caa7 | ||
|
|
28cab5a606 | ||
|
|
cfea56064d | ||
|
|
8467d87cfc | ||
|
|
b20d020bea | ||
|
|
948257c66e | ||
|
|
b54d1fb7fd | ||
|
|
ec361df0d1 | ||
|
|
b1a5cddde4 | ||
|
|
e165d38277 | ||
|
|
8ba340a8a5 | ||
|
|
d4e33663b2 | ||
|
|
d7d1b16dad | ||
|
|
0bc2ea13f2 | ||
|
|
b5d1301221 | ||
|
|
ed8f30ec71 | ||
|
|
a74a935ca0 | ||
|
|
7cfd56699b | ||
|
|
cb984237a7 | ||
|
|
c969fdddb9 | ||
|
|
9931ad2ce1 | ||
|
|
fd73feb645 | ||
|
|
ee78428a2a | ||
|
|
ae02249255 | ||
|
|
727af2e6fb | ||
|
|
8fd5576879 | ||
|
|
1f85dcee7c | ||
|
|
138890bc5c | ||
|
|
a094efc9e6 | ||
|
|
1f9e2fdecc | ||
|
|
4a2b4660bc | ||
|
|
b3ac90015a | ||
|
|
2fe06f0a4e | ||
|
|
1836a7484e | ||
|
|
25a5c5aaab | ||
|
|
24694e2558 | ||
|
|
2325edd9ba | ||
|
|
fad5713ade | ||
|
|
fe8573322f | ||
|
|
06c1255abe | ||
|
|
f108a67635 | ||
|
|
bf580d061d | ||
|
|
b005bd7b98 | ||
|
|
75f8baab33 | ||
|
|
5c3fb73cef | ||
|
|
5c3f4180b9 | ||
|
|
6cd6e7ceed | ||
|
|
1a146c2a64 | ||
|
|
eaeb9e6efa | ||
|
|
2e84c91748 | ||
|
|
650d45c1f4 | ||
|
|
f4f65024ef | ||
|
|
1200aa4fb8 | ||
|
|
6762363685 | ||
|
|
b2ead325c4 | ||
|
|
4e24b915cc | ||
|
|
b610ee26ba | ||
|
|
2b867f1613 | ||
|
|
7b8fe565c7 | ||
|
|
a246862910 | ||
|
|
106809f3fd | ||
|
|
f0d8499f7e | ||
|
|
332ca3d55e | ||
|
|
a48f5d5796 | ||
|
|
f04f047428 | ||
|
|
4e61fd33ea | ||
|
|
61ac77be72 | ||
|
|
c093eb5b63 | ||
|
|
98e24131bd | ||
|
|
7becce9e8c | ||
|
|
3cdaeb719a | ||
|
|
8daaea5969 | ||
|
|
dc47516e14 | ||
|
|
0fcc4f822f | ||
|
|
c0ed061ff5 | ||
|
|
d98b6b418d | ||
|
|
deea29b5e8 | ||
|
|
0bdbc83ed9 | ||
|
|
6c591f0990 | ||
|
|
b55b9c257b | ||
|
|
5156c21d14 | ||
|
|
a9d824753b | ||
|
|
3c6a208101 | ||
|
|
b1032a1ca4 | ||
|
|
931f34fccd | ||
|
|
f2509adec1 | ||
|
|
285b82eb65 | ||
|
|
74da197304 | ||
|
|
0f727248d2 | ||
|
|
a6de16f92f | ||
|
|
fc09854d7f | ||
|
|
2959029151 | ||
|
|
e590441b7b | ||
|
|
dc41ec7cb1 | ||
|
|
43049c865c | ||
|
|
c4a9fc7f88 | ||
|
|
faf4026cf4 | ||
|
|
f53f45a6cd | ||
|
|
e04e876f44 | ||
|
|
a84e7e30da | ||
|
|
6eed6ff779 | ||
|
|
1375211610 | ||
|
|
4e9369a702 | ||
|
|
f9e8748a96 | ||
|
|
20d6bf267a | ||
|
|
b573f9dab2 | ||
|
|
7ed4fe50d4 | ||
|
|
6f66ec1727 | ||
|
|
c7e758fc36 | ||
|
|
14c22234bb | ||
|
|
d565e9ae53 | ||
|
|
4951c97eab | ||
|
|
9b38f3e2fa | ||
|
|
dbc76389d8 | ||
|
|
c27f838444 | ||
|
|
ce84485e26 | ||
|
|
6cf254e2f9 | ||
|
|
02b63c28a5 | ||
|
|
57c6ce7ffa | ||
|
|
2f3272ea2f | ||
|
|
f5c2d57e4b | ||
|
|
baa878272d | ||
|
|
093285868e | ||
|
|
6c9d058ec2 | ||
|
|
5df7be6892 | ||
|
|
2deca816ae | ||
|
|
b8d2fceced | ||
|
|
7596d71460 | ||
|
|
096067b097 | ||
|
|
ec09505f6b | ||
|
|
251ea756c8 | ||
|
|
8f6544efe2 | ||
|
|
6045a8ad8c | ||
|
|
b184d62634 | ||
|
|
1a8d512abb | ||
|
|
a62be8ea32 | ||
|
|
c230d94ff0 | ||
|
|
e7b02773f5 | ||
|
|
ed83248a6b | ||
|
|
af8b4901d4 | ||
|
|
64c8230960 | ||
|
|
bf664534cc | ||
|
|
274a04e535 | ||
|
|
cb81f3d50e | ||
|
|
30a3b24287 | ||
|
|
8aacf71956 | ||
|
|
72d503d3a3 | ||
|
|
453a904290 | ||
|
|
368bff4fb4 | ||
|
|
4ae045d704 | ||
|
|
8c71939425 | ||
|
|
a437c2d365 | ||
|
|
a1784e3237 | ||
|
|
abee0f853c | ||
|
|
e9d358ed17 | ||
|
|
c5d54d06bb | ||
|
|
c16eed7ca2 | ||
|
|
76388a10b5 | ||
|
|
38bcc033a2 | ||
|
|
5af563cd91 | ||
|
|
3de271161c | ||
|
|
c19f9bc43a | ||
|
|
ef85d245ed | ||
|
|
25749bd4c0 | ||
|
|
e19c5464fe | ||
|
|
5c2ea3b804 | ||
|
|
c27348d470 | ||
|
|
de5f9c9217 | ||
|
|
f9086ee3a2 | ||
|
|
43298a9026 | ||
|
|
d80e228c6f | ||
|
|
2902362886 | ||
|
|
1cd303ad7f | ||
|
|
f590a476e7 | ||
|
|
e71cb3ba68 | ||
|
|
510a9af2e5 | ||
|
|
5328f84df4 | ||
|
|
18817fd81b | ||
|
|
4bcc536fd2 | ||
|
|
1ab2ddd317 | ||
|
|
09aa168840 | ||
|
|
05753fb207 | ||
|
|
715e3f8543 | ||
|
|
9c9d4b35a4 | ||
|
|
2ee935f784 | ||
|
|
58aedc88a4 | ||
|
|
0e60385871 | ||
|
|
a4188f7986 | ||
|
|
c7cbfe7a4f | ||
|
|
f1c9f5040b | ||
|
|
79e51051c7 | ||
|
|
a63d0da528 | ||
|
|
4fd8df208f | ||
|
|
44d3bd30fa | ||
|
|
6e6e932370 | ||
|
|
baccf50417 | ||
|
|
7b1071b30d | ||
|
|
bd7ca94196 | ||
|
|
1ec1aa76e9 | ||
|
|
77c369c3c7 | ||
|
|
9171d4b040 | ||
|
|
e02b95fca5 | ||
|
|
d45a07b5e5 | ||
|
|
0cdcfcee8d | ||
|
|
324546b4e7 | ||
|
|
c8ee67a636 | ||
|
|
b87c57c951 | ||
|
|
721f662bbe | ||
|
|
fccd48bfff | ||
|
|
5310d903ec | ||
|
|
8cbce555e4 | ||
|
|
f6112713e8 | ||
|
|
cc637f4dea | ||
|
|
7f76a14c54 | ||
|
|
58675f4d5a | ||
|
|
d50e6db312 | ||
|
|
de74284a8e | ||
|
|
4c9a295b28 | ||
|
|
0968f36d3e | ||
|
|
fd570b0377 | ||
|
|
68ea5ee570 | ||
|
|
f891140a74 | ||
|
|
5ed2d7ac2b | ||
|
|
a297e4208e | ||
|
|
b713527da0 | ||
|
|
224d2cedc8 | ||
|
|
55cfea776f | ||
|
|
d7a2078e0b | ||
|
|
a3e540eb32 | ||
|
|
e01c20be84 | ||
|
|
ce3ca418c2 | ||
|
|
15b9a5faf6 | ||
|
|
3afa30894f | ||
|
|
0ecfa827e6 | ||
|
|
e1b0db75eb | ||
|
|
b0c773189f | ||
|
|
3064326834 | ||
|
|
c67e50fe34 | ||
|
|
9d45e3eca1 | ||
|
|
43a24d15f6 | ||
|
|
cafbda1668 | ||
|
|
86c26fd64c | ||
|
|
0c20668008 | ||
|
|
92df8dc43c | ||
|
|
9d5f5844b8 | ||
|
|
2cf31884d0 | ||
|
|
19354c6f2d | ||
|
|
0b2079ad41 | ||
|
|
5f18c3af70 | ||
|
|
0a40285d43 | ||
|
|
5b1c328541 | ||
|
|
37929533af | ||
|
|
3b92113680 | ||
|
|
46b52cb9bb | ||
|
|
f0bcc9d9ba | ||
|
|
1cac028bfe | ||
|
|
4956886819 | ||
|
|
c720cfc7c7 | ||
|
|
8fcef5628f | ||
|
|
c4a72802f0 | ||
|
|
917394803c | ||
|
|
01040ddcdd | ||
|
|
7947497f7e | ||
|
|
539ca5856f | ||
|
|
89c801f82c | ||
|
|
3de4f22d34 | ||
|
|
0e4d2be98c | ||
|
|
d8ce108ccd | ||
|
|
d123cd4b2b | ||
|
|
4d34aa7cd6 | ||
|
|
b860e94582 | ||
|
|
9d653e3788 | ||
|
|
9e518cf2ba | ||
|
|
2856372ad6 | ||
|
|
efbf574613 | ||
|
|
c018eb2f0e | ||
|
|
d7bfe54b7c | ||
|
|
137282b7a9 | ||
|
|
769f8c8f34 | ||
|
|
8b8a37ae7c | ||
|
|
56e2b006f5 | ||
|
|
79cca05e43 | ||
|
|
166c8e8e82 | ||
|
|
9b64d2c325 | ||
|
|
03e3e9fae9 | ||
|
|
65234ae41a | ||
|
|
3828df8cf9 | ||
|
|
9cbe85bf99 | ||
|
|
7bf805b829 | ||
|
|
990ee436e1 | ||
|
|
1cd42066a6 | ||
|
|
ba43558049 | ||
|
|
951c8d34da | ||
|
|
ac61139243 | ||
|
|
5b8f1fe3e3 | ||
|
|
0aa197e4a4 | ||
|
|
f04e058c96 | ||
|
|
6ef2ae12b7 | ||
|
|
fe6bbdaefe | ||
|
|
cc66fddca9 | ||
|
|
04b70ddf13 | ||
|
|
bb3bb8d9c6 | ||
|
|
f80f62c7d1 | ||
|
|
2007ae4317 | ||
|
|
a1e5a1eff4 | ||
|
|
691999b402 | ||
|
|
33f3a4cea1 | ||
|
|
ab1d2dbe6a | ||
|
|
f622b281d0 | ||
|
|
fb12bf9b4c | ||
|
|
27af50087e | ||
|
|
03502bed52 | ||
|
|
27c7e2d150 | ||
|
|
e81d387971 | ||
|
|
ef1ade3a71 | ||
|
|
4f032f5b96 | ||
|
|
72cb967780 | ||
|
|
357934a644 | ||
|
|
327973657f | ||
|
|
d2730e6741 | ||
|
|
eb5ecab104 | ||
|
|
202055a9b8 | ||
|
|
7034a9e3fd | ||
|
|
1cf0b35ac1 | ||
|
|
8f7ed12262 | ||
|
|
96b5320ef9 | ||
|
|
d5cd742237 | ||
|
|
1f1da8942d | ||
|
|
7953e1e9d9 | ||
|
|
d6f7ecc0a3 | ||
|
|
3eed316049 | ||
|
|
851cf079c3 | ||
|
|
dfb0da32a9 | ||
|
|
f450da57e5 | ||
|
|
2ec6b6c995 | ||
|
|
53b769a8ec | ||
|
|
4f9adc173a | ||
|
|
dc4a58877e | ||
|
|
a6243a6fe7 | ||
|
|
cf5f1b541a | ||
|
|
70e6c48233 | ||
|
|
51f7d14d0a | ||
|
|
4853d5d1fc | ||
|
|
076a8938f0 | ||
|
|
5a3457ba33 | ||
|
|
2fc224384d | ||
|
|
a4e6ea5a3f | ||
|
|
d3c211f293 | ||
|
|
20047c369e | ||
|
|
dd1ff237a8 | ||
|
|
39d80d0b0e | ||
|
|
7a48316534 | ||
|
|
031a93ac46 | ||
|
|
ea6cc1aa95 | ||
|
|
365260ec44 | ||
|
|
2eb244c80a | ||
|
|
aee3011d61 | ||
|
|
40496e7b0f | ||
|
|
6b24f89fa7 | ||
|
|
2097800042 | ||
|
|
6739318e68 | ||
|
|
d0bd563d42 | ||
|
|
74280829fc | ||
|
|
3fde8880f2 | ||
|
|
98d39e0d38 | ||
|
|
c9cebb5ffe | ||
|
|
f52ac6e99c | ||
|
|
787a6b1c6a | ||
|
|
d00a91074e | ||
|
|
4e11497a38 | ||
|
|
0443d5202a | ||
|
|
633c25cb13 | ||
|
|
d07f45132f | ||
|
|
a51280afa6 | ||
|
|
be14eb2460 | ||
|
|
e26dbffcbe | ||
|
|
59992fd24a | ||
|
|
455362ccaf | ||
|
|
16c0e2460b | ||
|
|
c54084b7a4 | ||
|
|
92246f7125 | ||
|
|
e3fe040017 | ||
|
|
ae5e3e2dc4 | ||
|
|
77378d2779 | ||
|
|
4106f0dabe | ||
|
|
7737335ec9 | ||
|
|
5cc9b7e0d1 | ||
|
|
8c6a441064 | ||
|
|
fddc058ce2 | ||
|
|
89750086c5 | ||
|
|
e69406c7e2 | ||
|
|
878ae42d84 | ||
|
|
d34ebfc126 | ||
|
|
028f7b2d65 | ||
|
|
0aa3ec50f2 | ||
|
|
9146def21b | ||
|
|
ebb23a5a8c | ||
|
|
b118082984 | ||
|
|
b5c0ac5f25 | ||
|
|
dc78e874af | ||
|
|
c30bde0a2b | ||
|
|
171597fbe9 | ||
|
|
fae2d272d5 | ||
|
|
03a067d3e6 | ||
|
|
f5d028f3b3 | ||
|
|
e5b7dbba90 | ||
|
|
7ffba1e0b3 | ||
|
|
72cdbf0b78 | ||
|
|
8b4a86f629 | ||
|
|
fa15e64fc9 | ||
|
|
564f064c71 | ||
|
|
4062c7afa0 | ||
|
|
8071c4ba1c | ||
|
|
3d0ffbc832 | ||
|
|
1cac94bf97 | ||
|
|
c94c51d44f | ||
|
|
96958933af | ||
|
|
2300c2632e | ||
|
|
cbd0529674 | ||
|
|
5614e35ac4 | ||
|
|
c11172caba | ||
|
|
11b6e409bb | ||
|
|
3dca95aa3c | ||
|
|
7ddc706434 | ||
|
|
20eebb08e9 | ||
|
|
4abf41b85a | ||
|
|
e426f7ee7c | ||
|
|
14dc6a7984 | ||
|
|
e0a24a3f07 | ||
|
|
d1bee22d73 | ||
|
|
d73f7908f2 | ||
|
|
a4ea0d2b82 | ||
|
|
e2c15169b8 | ||
|
|
fe16ed3c73 | ||
|
|
80ce097f90 | ||
|
|
eceaf8a46b | ||
|
|
1e3fa4a9c7 | ||
|
|
2ed1ed6821 | ||
|
|
dc640a7591 | ||
|
|
1f072d182c | ||
|
|
1d64e04ed5 | ||
|
|
22f4f0b79e | ||
|
|
69c63293fb | ||
|
|
c1db13ceeb | ||
|
|
6d3a38842d | ||
|
|
70eadee0aa | ||
|
|
7360f79413 | ||
|
|
228afe01ed | ||
|
|
61a5154e49 | ||
|
|
d3df75aaa0 | ||
|
|
c59180dd6e | ||
|
|
e4c2310632 | ||
|
|
e1735a2da1 | ||
|
|
c101c9c8e1 | ||
|
|
96dc162de5 | ||
|
|
257dbe3104 | ||
|
|
cd98657e3c | ||
|
|
03eb22fe0a | ||
|
|
8d55e13750 | ||
|
|
737e8e79c9 | ||
|
|
4d977fede0 | ||
|
|
0073a868d4 | ||
|
|
0bb61d72ab | ||
|
|
f758508a82 | ||
|
|
69d0218d7e | ||
|
|
eb5e5ab1df | ||
|
|
093697906c | ||
|
|
efe96b7ed1 | ||
|
|
7ecdd41ab9 | ||
|
|
aec70d61e9 | ||
|
|
2efac13344 | ||
|
|
15aeb11c36 | ||
|
|
e705f4d984 | ||
|
|
96fa62fdfe | ||
|
|
845c70797a | ||
|
|
16048956c3 | ||
|
|
cf2f4b5902 | ||
|
|
db46f33f34 | ||
|
|
25d1515daf | ||
|
|
a3469cd59f | ||
|
|
513ce26200 | ||
|
|
1cd96f94ff | ||
|
|
901dd041f0 | ||
|
|
a2ee94651e | ||
|
|
abdce063f1 | ||
|
|
a33ce5e4bf | ||
|
|
c9575eaef9 | ||
|
|
1e74476a71 | ||
|
|
82935884c4 | ||
|
|
d774a23768 | ||
|
|
e9f041e170 | ||
|
|
1f51b6e4f1 | ||
|
|
028650249c | ||
|
|
534197239f | ||
|
|
d2f4bb574c | ||
|
|
25ff8ef37b | ||
|
|
07fb1a2c39 | ||
|
|
581b800c43 | ||
|
|
30ca39287f | ||
|
|
01fa9698de | ||
|
|
10bd969636 | ||
|
|
f7761f2b61 | ||
|
|
49ff38a21f | ||
|
|
8d161306c7 | ||
|
|
027a82dff1 | ||
|
|
cb409d58e0 | ||
|
|
094e2f8151 | ||
|
|
71d121aeb9 | ||
|
|
b1a88af43c | ||
|
|
f73eb4ebd9 | ||
|
|
31ca9be299 | ||
|
|
02cc6f3d56 | ||
|
|
1642c082d1 | ||
|
|
892d213442 | ||
|
|
fc24267e09 | ||
|
|
9b71bdc608 | ||
|
|
310be89895 | ||
|
|
71fbd57e12 | ||
|
|
ab4b48c823 | ||
|
|
532767cfa1 | ||
|
|
5512de3221 | ||
|
|
13546d5e8f | ||
|
|
c6f1aa8086 | ||
|
|
5606c47cb7 | ||
|
|
7f7cd96211 | ||
|
|
b828bfd890 | ||
|
|
31d084eb78 | ||
|
|
ab18b280e9 | ||
|
|
24e89c4081 | ||
|
|
e129390f56 | ||
|
|
4d7c87bb4c | ||
|
|
dac3f82a75 | ||
|
|
fd860921f1 | ||
|
|
0482ccd48b | ||
|
|
b8b1990617 | ||
|
|
70951b1198 | ||
|
|
6d24514ace | ||
|
|
49915ceb84 | ||
|
|
925b13e337 | ||
|
|
ef3143d558 | ||
|
|
ed84637b55 | ||
|
|
897a944478 | ||
|
|
d86343c38d | ||
|
|
297afdd126 | ||
|
|
f0cbdc4e68 | ||
|
|
40b52cadde | ||
|
|
04bf85ddfe | ||
|
|
4809684a13 | ||
|
|
1eb50ad88f | ||
|
|
52569bcdb2 | ||
|
|
a50a407415 | ||
|
|
9f223442c2 | ||
|
|
c647114bb9 | ||
|
|
43719ec737 | ||
|
|
8602557985 | ||
|
|
dd1f7d0875 | ||
|
|
ec39e794d3 | ||
|
|
7b1a937d4c | ||
|
|
0fd38d8115 | ||
|
|
7a4efc6212 | ||
|
|
2eb2c5a413 | ||
|
|
2fcfb0aa9f | ||
|
|
f1df079512 | ||
|
|
8070e156d8 | ||
|
|
d77bedbafb | ||
|
|
43c6f1f5cd | ||
|
|
f53f5445ba | ||
|
|
b34c593c54 | ||
|
|
62efbc3342 | ||
|
|
2d609a0bde | ||
|
|
6bc4b4a17f | ||
|
|
b489e52080 | ||
|
|
7263d11ee4 | ||
|
|
f2d5b9ad69 | ||
|
|
40c7e3c52c | ||
|
|
a8aaeec52b | ||
|
|
ad7eec181e | ||
|
|
b33897ffb9 | ||
|
|
1c3d3f2f4b | ||
|
|
9a5a1edb6b | ||
|
|
f2eb869b02 | ||
|
|
0c7e3cfcb2 | ||
|
|
24e19db29e | ||
|
|
bc6d7b7bbd | ||
|
|
cad271068e | ||
|
|
3425293115 | ||
|
|
20dbfec3a9 | ||
|
|
170057a75a | ||
|
|
b86b761e0b | ||
|
|
da0d2f0266 | ||
|
|
321ea27c34 | ||
|
|
b712e6b9aa | ||
|
|
b3652e6527 | ||
|
|
bc97f397ef | ||
|
|
e5da3f6e68 | ||
|
|
8400539acf | ||
|
|
b5eac8dfed | ||
|
|
ba312b5591 | ||
|
|
f23572b318 | ||
|
|
db838634e7 | ||
|
|
7f2e848a5c | ||
|
|
096e854d50 | ||
|
|
3ffe8b3155 | ||
|
|
a471f49b61 | ||
|
|
4d2a02f318 | ||
|
|
0bec7db03b | ||
|
|
74827f983f | ||
|
|
0ed46f457e | ||
|
|
36b731be73 | ||
|
|
62fbdd4e81 | ||
|
|
ca7b0650c2 | ||
|
|
67dd146038 | ||
|
|
fb66df2efd | ||
|
|
2395ca0057 | ||
|
|
d203789490 | ||
|
|
7ea0e31cd4 | ||
|
|
d3bf13a503 | ||
|
|
ea91970499 | ||
|
|
803b3f2cc4 | ||
|
|
1788ba6c5c | ||
|
|
5209bd3d9f | ||
|
|
cb9178f1ec | ||
|
|
5676920a6a | ||
|
|
513221d9fd | ||
|
|
a33d0b4b53 | ||
|
|
bee242b781 | ||
|
|
fa1c98ff29 | ||
|
|
ae3a7d9bed | ||
|
|
ee5fea4221 | ||
|
|
db7b60cfe9 | ||
|
|
0c2efb312c | ||
|
|
51b79bd6a1 | ||
|
|
95fe762776 | ||
|
|
cf8eeaab0b | ||
|
|
2f8cb3ce76 | ||
|
|
821da723c0 | ||
|
|
575b97ba60 | ||
|
|
cc0819b709 | ||
|
|
318d6f042b | ||
|
|
05ae3a1703 | ||
|
|
8e54805e62 | ||
|
|
64399a72f3 | ||
|
|
6c33f0b0bd | ||
|
|
aca304b395 | ||
|
|
db5c9e67be | ||
|
|
2313cec792 | ||
|
|
2968c846ce | ||
|
|
83acaf692a | ||
|
|
e9aeb2662b | ||
|
|
356f4039e4 | ||
|
|
736c7f1f30 | ||
|
|
2994448036 | ||
|
|
d476d9ea05 | ||
|
|
bf31bce440 | ||
|
|
6393e89022 | ||
|
|
884268fce3 | ||
|
|
071a9307c9 | ||
|
|
2cdfaa0a82 | ||
|
|
ecf878e14d | ||
|
|
4eed335bc7 | ||
|
|
2e57bb74d2 | ||
|
|
0a39769cd0 | ||
|
|
bdb6a9e5d1 | ||
|
|
f88e0eb96d | ||
|
|
0099f60d29 | ||
|
|
eaf9f20c56 | ||
|
|
e987c4741a | ||
|
|
6242278abd | ||
|
|
30850a431a | ||
|
|
1e7407c042 | ||
|
|
6d94f31ff2 | ||
|
|
ebb3d1cfd3 | ||
|
|
acce9489d7 | ||
|
|
3d442620f9 | ||
|
|
f1d7eb8565 | ||
|
|
798b935ff6 | ||
|
|
3039a1444e | ||
|
|
aa7d15beb3 | ||
|
|
2b3d2cb342 | ||
|
|
5a58357429 | ||
|
|
366add2536 | ||
|
|
e13c9fd42e | ||
|
|
2a6c01f634 | ||
|
|
bf29722e78 | ||
|
|
db227ad15f | ||
|
|
514716042b | ||
|
|
7a767e680c | ||
|
|
320b52eb1e | ||
|
|
428cee75c5 | ||
|
|
5479a55b2c | ||
|
|
d1f2a5d04f | ||
|
|
09ba319f3e | ||
|
|
3da711ba8b | ||
|
|
6f524fb816 | ||
|
|
d3e2a9e5c0 | ||
|
|
b4cd7d7941 | ||
|
|
cd03b91115 | ||
|
|
f86d002ceb | ||
|
|
940926b5ec | ||
|
|
85c096df0b | ||
|
|
76d93522ac | ||
|
|
31492831cc | ||
|
|
8221dd594e | ||
|
|
6346ca1a84 | ||
|
|
4a3404883f | ||
|
|
1ebca35313 | ||
|
|
e0d1381f87 | ||
|
|
86e6841569 | ||
|
|
28b7a92a00 | ||
|
|
4db5b18694 | ||
|
|
a628e921c0 | ||
|
|
6ca6ff37c9 | ||
|
|
456db3710a | ||
|
|
50f024c6f9 | ||
|
|
a4de75a8c0 | ||
|
|
88e8fcdaca | ||
|
|
bfe9952c9a | ||
|
|
7f568e3e7e | ||
|
|
9b8800ac1d | ||
|
|
fd53712567 | ||
|
|
7f74c2465c | ||
|
|
30d67a78eb | ||
|
|
c3cfd1f0ce | ||
|
|
69ac70eed8 | ||
|
|
fcf49e79cc | ||
|
|
8d4894846d | ||
|
|
a809b710c5 | ||
|
|
f6289e9db2 | ||
|
|
26b4c4df22 | ||
|
|
f3a9844295 | ||
|
|
692821bdae | ||
|
|
ee143d5b3a | ||
|
|
7e178a634a | ||
|
|
fe88a3d80b | ||
|
|
a196eac290 | ||
|
|
3c819955a2 | ||
|
|
ca0d7bbbed | ||
|
|
f93bd1e817 | ||
|
|
415bc6ca0a | ||
|
|
8543c8d11d | ||
|
|
bf5ad64575 | ||
|
|
d42d02d809 | ||
|
|
0718f79ff2 | ||
|
|
9bbce225ce | ||
|
|
fb35fd6d71 | ||
|
|
b4fd92aed6 | ||
|
|
36931825b3 | ||
|
|
ca35299dcd | ||
|
|
e74b900914 | ||
|
|
25115668a7 | ||
|
|
fb94db3e64 | ||
|
|
c4778e770e | ||
|
|
3860cdf97b | ||
|
|
f3aec0c4ac | ||
|
|
d333094149 | ||
|
|
609ff4e66c | ||
|
|
cbccbcd9e7 | ||
|
|
54b1d7fcc1 | ||
|
|
54388c0d9b | ||
|
|
228c866aaa | ||
|
|
a09bd648af | ||
|
|
3e4ae61c75 | ||
|
|
7655c432c2 | ||
|
|
25dd651757 | ||
|
|
462aecea3e | ||
|
|
5f37df790b | ||
|
|
8e4e03541c | ||
|
|
c1252fc7eb | ||
|
|
ed1077cc9a | ||
|
|
4c761a7b22 | ||
|
|
9bc3df7803 | ||
|
|
5e5060a6fe | ||
|
|
2b66eddaa1 | ||
|
|
916b9d6c6d | ||
|
|
bd09ccd608 | ||
|
|
682f8e4d45 | ||
|
|
c9d0af9ee0 | ||
|
|
e1299d59bf | ||
|
|
61da6437ea | ||
|
|
798705469b | ||
|
|
459a753de3 | ||
|
|
1092ce70b3 | ||
|
|
9511c189bd | ||
|
|
66fea9e2ee | ||
|
|
69ae83516e | ||
|
|
144ea36c81 | ||
|
|
7a8ab9a900 | ||
|
|
c4b35055b4 | ||
|
|
a4c04e7c17 | ||
|
|
a6f7e7fc30 | ||
|
|
d5ebc883b3 | ||
|
|
deb43df0a4 | ||
|
|
88e472b3f1 | ||
|
|
f59fb8167d | ||
|
|
fac6f526f7 | ||
|
|
2f78d74ce6 | ||
|
|
d3942dda52 | ||
|
|
c00e9a8d3a | ||
|
|
c3b95767f3 | ||
|
|
90f27a3090 | ||
|
|
b6f09defc9 | ||
|
|
172813bcfb | ||
|
|
95c25efab7 | ||
|
|
a51af35024 | ||
|
|
119fd5ba7d | ||
|
|
0718a812bd | ||
|
|
3814501b48 | ||
|
|
7a5205dbda | ||
|
|
15a5028d23 | ||
|
|
fee2648ac0 | ||
|
|
04c02c9a20 | ||
|
|
e27da96cdc | ||
|
|
0ff7195a83 | ||
|
|
3b91aa013a | ||
|
|
50f6235edb | ||
|
|
6f4d94f91b | ||
|
|
83a4c7d443 | ||
|
|
8171fec925 | ||
|
|
175f352ea7 | ||
|
|
5290161ac4 | ||
|
|
8762019ed7 | ||
|
|
61a59fa158 | ||
|
|
55eea20c8e | ||
|
|
9a621f0c54 | ||
|
|
55fc24e933 | ||
|
|
b14608f09b | ||
|
|
4a25c57337 | ||
|
|
f800e35ccb | ||
|
|
12d49a9b9d | ||
|
|
b25b251a44 | ||
|
|
64b2a75a94 | ||
|
|
b33a60f3a5 | ||
|
|
d22dbb1a6d | ||
|
|
983199a6cd | ||
|
|
133d7ee33a | ||
|
|
0bd888afc7 | ||
|
|
537bd1c58d | ||
|
|
5ef519fe2c | ||
|
|
20498fb47f | ||
|
|
b57dfb3b5d | ||
|
|
0355ed4aa1 | ||
|
|
1e76cc7bdc | ||
|
|
18c0374126 | ||
|
|
7072fba7e7 | ||
|
|
3d702a5c39 | ||
|
|
f31efa42c9 | ||
|
|
d86502e79a | ||
|
|
59c7744590 | ||
|
|
949971dea9 | ||
|
|
cd4a893c65 | ||
|
|
74b369ff20 | ||
|
|
46eed0a59a | ||
|
|
9643296e29 | ||
|
|
c83c5b5a34 | ||
|
|
277e2d7fc0 | ||
|
|
7280e390d9 | ||
|
|
4efc3f0a39 | ||
|
|
cb7e7a8aa3 | ||
|
|
9136402846 | ||
|
|
260fc76137 | ||
|
|
7cfb9a4d15 | ||
|
|
2089e0c974 | ||
|
|
9e0b4fe5d1 | ||
|
|
75ce632f84 | ||
|
|
efeb96c4e8 | ||
|
|
fb5438e9c2 | ||
|
|
7da9f66e1c | ||
|
|
9e16e3d614 | ||
|
|
84d040c6d0 | ||
|
|
f3e0beb8f1 | ||
|
|
e00a1196ef | ||
|
|
3867c0f8e7 | ||
|
|
cdf0953722 | ||
|
|
ed00f7d071 | ||
|
|
a3038afa02 | ||
|
|
f9ca0b8cc6 | ||
|
|
2920aa5af4 | ||
|
|
93c9cc4a0e | ||
|
|
b53f9235e4 | ||
|
|
1491462d15 | ||
|
|
c78f779800 | ||
|
|
b013e375fb | ||
|
|
52036138c1 | ||
|
|
4ba9a42861 | ||
|
|
27bff7a759 | ||
|
|
896f8d85f7 | ||
|
|
ed06cdd2c7 | ||
|
|
8473647269 | ||
|
|
5579145a06 | ||
|
|
35848d10b3 | ||
|
|
c7e223e85a | ||
|
|
885b2d1d2f | ||
|
|
73020be511 | ||
|
|
d388c057c0 | ||
|
|
c4d0f91a7f | ||
|
|
467233be04 | ||
|
|
2b02d08f4c | ||
|
|
9fe265ea64 | ||
|
|
cc1f4ba81c | ||
|
|
3784bdbd27 | ||
|
|
4ffdc3b77c | ||
|
|
38c9fa681a | ||
|
|
c477039954 | ||
|
|
d6ef3d64ac | ||
|
|
6938152db6 | ||
|
|
2154db07f0 | ||
|
|
5e0803479e | ||
|
|
3960c604a4 | ||
|
|
394648f1c9 | ||
|
|
da5c4953d5 | ||
|
|
2b7e1cb5b1 | ||
|
|
f182eafb40 | ||
|
|
9f7f42e885 | ||
|
|
9b8bce1914 | ||
|
|
96d05e12fc | ||
|
|
68c1069548 | ||
|
|
5b64613f65 | ||
|
|
1f9baefba8 | ||
|
|
0c255d2618 | ||
|
|
a38206de9c | ||
|
|
260f7c9b85 | ||
|
|
de294caed9 | ||
|
|
e40aa4f99a | ||
|
|
b1d413b9be | ||
|
|
8cbad070ad | ||
|
|
13569a5a5a | ||
|
|
d789334a60 | ||
|
|
7668b27fc0 | ||
|
|
6d30f441e8 | ||
|
|
a9e395b366 | ||
|
|
5e5626f04f | ||
|
|
d80aa5b44e | ||
|
|
80ef6dc4de | ||
|
|
458549f7df | ||
|
|
a8405649d0 | ||
|
|
ce1a72850b | ||
|
|
58de381746 | ||
|
|
bed2e894a2 | ||
|
|
b4de98cfb7 | ||
|
|
a4b9db9e07 | ||
|
|
664111a3c9 | ||
|
|
aa964847f3 | ||
|
|
fa5cac7e0a | ||
|
|
b2b01861b2 | ||
|
|
f014f718eb | ||
|
|
05ae8d3ffa | ||
|
|
88c9e08bd8 | ||
|
|
844f61dfea | ||
|
|
acb7d597cb | ||
|
|
2b18f60261 | ||
|
|
5b66133a6c | ||
|
|
0c5bc6a57a | ||
|
|
7981e00955 | ||
|
|
5e39c0cfeb | ||
|
|
a444701929 | ||
|
|
f6c1eb5d9d | ||
|
|
a1d46cb26b | ||
|
|
99ab148d88 | ||
|
|
d69fa5dba5 | ||
|
|
0d30b000af | ||
|
|
e7c0e742d2 | ||
|
|
2aff2dcca3 | ||
|
|
288f8865c8 | ||
|
|
8691870bcb | ||
|
|
e06146c237 | ||
|
|
c68e990cda | ||
|
|
4583905313 | ||
|
|
9cc498b1fa | ||
|
|
b3c5dc4045 | ||
|
|
56ca7360ae | ||
|
|
d5ab3251f0 | ||
|
|
915c284420 | ||
|
|
3824da7261 | ||
|
|
40154824e8 | ||
|
|
855d567b1e | ||
|
|
b323a7bd88 | ||
|
|
fa011d0018 | ||
|
|
e15fa8777a | ||
|
|
2143a6d927 | ||
|
|
044e2d3e73 | ||
|
|
be112ec63f | ||
|
|
d2f56c4e8f | ||
|
|
ddc6a9c695 | ||
|
|
2bebdbc371 | ||
|
|
8b9f1f0608 | ||
|
|
b25f3b2ed2 | ||
|
|
a995cf81b6 | ||
|
|
75d261639f | ||
|
|
f720d795d0 | ||
|
|
f6fe83e358 | ||
|
|
0513d0b6a8 | ||
|
|
0679bb217d | ||
|
|
38bd55e518 | ||
|
|
65c7423280 | ||
|
|
f24a85cc94 | ||
|
|
53887b7c98 | ||
|
|
523c012c38 | ||
|
|
97c28989c1 | ||
|
|
c19be6ebb2 | ||
|
|
54971a0735 | ||
|
|
4513e81e13 | ||
|
|
872204b795 | ||
|
|
a94cbfe6f5 | ||
|
|
7152faafb2 | ||
|
|
e6aadaccd8 | ||
|
|
3a73aa71b8 | ||
|
|
814e7509e1 | ||
|
|
e0cf5ec016 | ||
|
|
667bd32e6a | ||
|
|
b2ecd83706 | ||
|
|
b2754117c8 | ||
|
|
6c428c303b | ||
|
|
e7d889a143 | ||
|
|
da60e7069b | ||
|
|
c14406a3b9 | ||
|
|
725ab5ec21 | ||
|
|
daf9d47e58 | ||
|
|
63a65627a2 | ||
|
|
02c07755b0 | ||
|
|
15cbd18acc | ||
|
|
93c40b87dc | ||
|
|
eeaa9f67a1 | ||
|
|
b60691c7b2 | ||
|
|
2bb1b0b343 | ||
|
|
047ef9f86c | ||
|
|
9a2c603c91 | ||
|
|
94c4169407 | ||
|
|
cb8a551db8 | ||
|
|
779f09af70 | ||
|
|
19dc0f2bfb | ||
|
|
f0709e22ba | ||
|
|
8250736f5e | ||
|
|
83348a9f93 | ||
|
|
96d40903a9 | ||
|
|
2560811805 | ||
|
|
2b8c44c008 | ||
|
|
38e2d37674 | ||
|
|
6278561f88 | ||
|
|
750e79c1ce | ||
|
|
71eb2963c5 | ||
|
|
f44e2c86ea | ||
|
|
afe1f0df8c | ||
|
|
458fddfb48 | ||
|
|
8d915c5ccb | ||
|
|
304153dd03 | ||
|
|
a6781b7352 | ||
|
|
5ad0058303 | ||
|
|
75c039de33 | ||
|
|
74e3c3677e | ||
|
|
dc20327f10 | ||
|
|
e738affd29 | ||
|
|
ef3d732607 | ||
|
|
6d63cff1bf | ||
|
|
12f42605a1 | ||
|
|
fac3337927 | ||
|
|
76d198151c | ||
|
|
6a907058de | ||
|
|
6e1f531f64 | ||
|
|
4232cca5b6 | ||
|
|
cf2f249f8a | ||
|
|
c510870736 | ||
|
|
e8783f6a33 | ||
|
|
8cda4512ad | ||
|
|
fc90bdc638 | ||
|
|
5a88165a26 | ||
|
|
3466842cd4 |
18
.github/workflows/android.yaml
vendored
@@ -6,11 +6,13 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- "examples/simple-chatbot/client/android/**"
|
- "examples/simple-chatbot/client/android/**"
|
||||||
|
- "examples/p2p-webrtc/video-transform/client/android/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
paths:
|
paths:
|
||||||
- "examples/simple-chatbot/client/android/**"
|
- "examples/simple-chatbot/client/android/**"
|
||||||
|
- "examples/p2p-webrtc/video-transform/client/android/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
sdk_git_ref:
|
sdk_git_ref:
|
||||||
@@ -23,7 +25,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sdk:
|
sdk:
|
||||||
name: "Simple chatbot demo"
|
name: "Demo apps"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -37,12 +39,22 @@ jobs:
|
|||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
- name: Build demo app
|
- name: "Example app: Simple Chatbot"
|
||||||
working-directory: examples/simple-chatbot/client/android
|
working-directory: examples/simple-chatbot/client/android
|
||||||
run: ./gradlew :simple-chatbot-client:assembleDebug
|
run: ./gradlew :simple-chatbot-client:assembleDebug
|
||||||
|
|
||||||
- name: Upload demo APK
|
- name: Upload Simple Chatbot APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Simple Chatbot Android Client
|
name: Simple Chatbot Android Client
|
||||||
path: examples/simple-chatbot/client/android/simple-chatbot-client/build/outputs/apk/debug/simple-chatbot-client-debug.apk
|
path: examples/simple-chatbot/client/android/simple-chatbot-client/build/outputs/apk/debug/simple-chatbot-client-debug.apk
|
||||||
|
|
||||||
|
- name: "Example app: Small WebRTC Client"
|
||||||
|
working-directory: examples/p2p-webrtc/video-transform/client/android
|
||||||
|
run: ./gradlew :small-webrtc-client:assembleDebug
|
||||||
|
|
||||||
|
- name: Upload Small WebRTC APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Small WebRTC Android Client
|
||||||
|
path: examples/p2p-webrtc/video-transform/client/android/small-webrtc-client/build/outputs/apk/debug/small-webrtc-client-debug.apk
|
||||||
|
|||||||
6
.github/workflows/format.yaml
vendored
@@ -17,7 +17,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ruff-format:
|
ruff-format:
|
||||||
name: "Formatting checker"
|
name: "Code quality checks"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -39,8 +39,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
ruff format --diff
|
ruff format --diff
|
||||||
- name: Ruff import linter
|
- name: Ruff linter (all rules)
|
||||||
id: ruff-check
|
id: ruff-check
|
||||||
run: |
|
run: |
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
ruff check --select I
|
ruff check
|
||||||
|
|||||||
2
.github/workflows/publish.yaml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
gitref:
|
gitref:
|
||||||
type: string
|
type: string
|
||||||
description: "what git ref to build"
|
description: "what git tag to build (e.g. v0.0.74)"
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
language_version: python3
|
language_version: python3
|
||||||
args: [ --select, I, ]
|
args: [--fix]
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|||||||
920
CHANGELOG.md
@@ -9,6 +9,894 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added a new field `handle_sigterm` to `PipelineRunner`. It defaults to `False`.
|
||||||
|
This field handles SIGTERM signals. The `handle_sigint` field still defaults
|
||||||
|
to `True`, but now it handles only SIGINT signals.
|
||||||
|
|
||||||
|
- Added foundational example `14u-function-calling-ollama.py` for Ollama
|
||||||
|
function calling.
|
||||||
|
|
||||||
|
- Added `LocalSmartTurnAnalyzerV2`, which supports local on-device inference
|
||||||
|
with the new `smart-turn-v2` turn detection model.
|
||||||
|
|
||||||
|
- Added `set_log_level` to `DailyTransport`, allowing setting the logging level
|
||||||
|
for Daily's internal logging system.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Play delayed messages from `ElevenLabsTTSService` if they still belong to the
|
||||||
|
current context.
|
||||||
|
|
||||||
|
- Dependency compatibility improvements: Relaxed version constraints for core
|
||||||
|
dependencies to support broader version ranges while maintaining stability:
|
||||||
|
|
||||||
|
- `aiohttp`, `Markdown`, `nltk`, `numpy`, `Pillow`, `pydantic`, `openai`,
|
||||||
|
`numba`: Now support up to the next major version (e.g. `numpy>=1.26.4,<3`)
|
||||||
|
- `pyht`: Relaxed to `>=0.1.6` to resolve `grpcio` conflicts with
|
||||||
|
`nvidia-riva-client`
|
||||||
|
- `fastapi`: Updated to support versions `>=0.115.6,<0.117.0`
|
||||||
|
- `torch`/`torchaudio`: Changed from exact pinning (`==2.5.0`) to compatible
|
||||||
|
range (`~=2.5.0`)
|
||||||
|
- `aws_sdk_bedrock_runtime`: Added Python 3.12+ constraint via environment
|
||||||
|
marker
|
||||||
|
- `numba`: Reduced minimum version to `0.60.0` for better compatibility
|
||||||
|
|
||||||
|
- Changed `NeuphonicHttpTTSService` to use a POST based request instead of the
|
||||||
|
`pyneuphonic` package. This removes a package requirement, allowing Neuphonic
|
||||||
|
to work with more services.
|
||||||
|
|
||||||
|
- Updated the `deepgram` optional dependency to 4.7.0, which downgrades the
|
||||||
|
`tasks cancelled error` to a debug log. This removes the log from appearing
|
||||||
|
in Pipecat logs upon leaving.
|
||||||
|
|
||||||
|
- Upgraded the `websockets` implementation to the new asyncio implementation.
|
||||||
|
Along with this change, we're updating support for versions >=13.1.0 and
|
||||||
|
<15.0.0. All services have been update to use the asyncio implementation.
|
||||||
|
|
||||||
|
- Updated `MiniMaxHttpTTSService` with a `base_url` arg where you can specify
|
||||||
|
the Global endpoint (default) or Mainland China.
|
||||||
|
|
||||||
|
- Replaced regex-based sentence detection in `match_endofsentence` with NLTK's
|
||||||
|
punkt_tab tokenizer for more reliable sentence boundary detection.
|
||||||
|
|
||||||
|
- Changed the `livekit` optional dependency for `tenacity` to
|
||||||
|
`tenacity>=8.2.3,<10.0.0` in order to support the `google-genai` package.
|
||||||
|
|
||||||
|
- For `LmntTTSService`, changed the default `model` to `blizzard`, LMNT's
|
||||||
|
recommended model.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a dependency issue for uv users where an `llvmlite` version required python 3.9.
|
||||||
|
|
||||||
|
- Fixed an issue in `MiniMaxHttpTTSService` where the `pitch` param was the
|
||||||
|
incorrect type.
|
||||||
|
|
||||||
|
- Fixed an issue with OpenTelemetry tracing where the `enable_tracing` flag did
|
||||||
|
not disable the internal tracing decorator functions.
|
||||||
|
|
||||||
|
- Fixed an issue in `OLLamaLLMService` where kwargs were not passed correctly
|
||||||
|
to the parent class.
|
||||||
|
|
||||||
|
- Fixed an issue in `ElevenLabsTTSService` where the word/timestamp pairs were
|
||||||
|
calculating word boundaries incorrectly.
|
||||||
|
|
||||||
|
- Fixed an issue where, in some edge cases, the `EmulateUserStartedSpeakingFrame`
|
||||||
|
could be created even if we didn't have a transcription.
|
||||||
|
|
||||||
|
- Fixed an issue in `GoogleLLMContext` where it would inject the
|
||||||
|
`system_message` as a "user" message into cases where it was not meant to;
|
||||||
|
it was only meant to do that when there were no "regular" (non-function-call)
|
||||||
|
messages in the context, to ensure that inference would run properly.
|
||||||
|
|
||||||
|
- Fixed an issue in `LiveKitTransport` where the `on_audio_track_subscribed` was never emitted.
|
||||||
|
|
||||||
|
## [0.0.76] - 2025-07-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `SpeechControlParamsFrame`, a new `SystemFrame` that notifies
|
||||||
|
downstream processors of the VAD and Turn analyzer params. This frame is
|
||||||
|
pushed by the `BaseInputTransport` at Start and any time a
|
||||||
|
`VADParamsUpdateFrame` is received.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Two package dependencies have been updated:
|
||||||
|
- `numpy` now supports 1.26.0 and newer
|
||||||
|
- `transformers` now supports 4.48.0 and newer
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with RTVI's handling of `append-to-context`.
|
||||||
|
|
||||||
|
- Fixed an issue where using audio input with a sample rate requiring resampling
|
||||||
|
could result in empty audio being passed to STT services, causing errors.
|
||||||
|
|
||||||
|
- Fixed the VAD analyzer to process the full audio buffer as long as it contains
|
||||||
|
more than the minimum required bytes per iteration, instead of only analyzing
|
||||||
|
the first chunk.
|
||||||
|
|
||||||
|
- Fixed an issue in ParallelPipeline that caused errors when attempting to drain
|
||||||
|
the queues.
|
||||||
|
|
||||||
|
- Fixed an issue with emulated VAD timeout inconsistency in
|
||||||
|
`LLMUserContextAggregator`. Previously, emulated VAD scenarios (where
|
||||||
|
transcription is received without VAD detection) used a hardcoded
|
||||||
|
`aggregation_timeout` (default 0.5s) instead of matching the VAD's
|
||||||
|
`stop_secs` parameter (default 0.8s). This created different user experiences
|
||||||
|
between real VAD and emulated VAD scenarios. Now, emulated VAD timeouts
|
||||||
|
automatically synchronize with the VAD's `stop_secs` parameter.
|
||||||
|
|
||||||
|
- Fix a pipeline freeze when using AWS Nova Sonic, which would occur if the
|
||||||
|
user started early, while the bot was still working through
|
||||||
|
`trigger_assistant_response()`.
|
||||||
|
|
||||||
|
## [0.0.75] - 2025-07-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an `aggregate_sentences` arg in `CartesiaTTSService`,
|
||||||
|
`ElevenLabsTTSService`, `NeuphonicTTSService` and `RimeTTSService`, where the
|
||||||
|
default value is True. When `aggregate_sentences` is True, the `TTSService`
|
||||||
|
aggregates the LLM streamed tokens into sentences by default. Note: setting
|
||||||
|
the value to False requires a custom processor before the `TTSService` to
|
||||||
|
aggregate LLM tokens.
|
||||||
|
|
||||||
|
- Added `kwargs` to the `OLLamaLLMService` to allow for configuration args to
|
||||||
|
be passed to Ollama.
|
||||||
|
|
||||||
|
- Added call hang-up error handling in `TwilioFrameSerializer`, which handles
|
||||||
|
the case where the user has hung up before the `TwilioFrameSerializer` hangs
|
||||||
|
up the call.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated `RTVIObserver` and `RTVIProcessor` to match the new RTVI 1.0.0 protocol.
|
||||||
|
This includes:
|
||||||
|
|
||||||
|
- Deprecating support for all messages related to service configuaration and
|
||||||
|
actions.
|
||||||
|
- Adding support for obtaining and logging data about client, including its
|
||||||
|
RTVI version and optionally included system information (OS/browser/etc.)
|
||||||
|
- Adding support for handling the new `client-message` RTVI message through
|
||||||
|
either a `on_client_message` event handler or listening for a new
|
||||||
|
`RTVIClientMessageFrame`
|
||||||
|
- Adding support for responding to a `client-message` with a `server-response`
|
||||||
|
via either a direct call on the `RTVIProcessor` or via pushing a new
|
||||||
|
`RTVIServerResponseFrame`
|
||||||
|
- Adding built-in support for handling the new `append-to-context` RTVI message
|
||||||
|
which allows a client to add to the user or assistant llm context. No extra
|
||||||
|
code is required for supporting this behavior.
|
||||||
|
- Updating all JavaScript and React client RTVI examples to use versions 1.0.0
|
||||||
|
of the clients.
|
||||||
|
|
||||||
|
Get started migrating to RTVI protocol 1.0.0 by following the migration guide:
|
||||||
|
https://docs.pipecat.ai/client/migration-guide
|
||||||
|
|
||||||
|
- Refactored `AWSBedrockLLMService` and `AWSPollyTTSService` to work
|
||||||
|
asynchronously using `aioboto3` instead of the `boto3` library.
|
||||||
|
|
||||||
|
- The `UserIdleProcessor` now handles the scenario where function calls take
|
||||||
|
longer than the idle timeout duration. This allows you to use the
|
||||||
|
`UserIdleProcessor` in conjunction with function calls that take a while to
|
||||||
|
return a result.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Updated the `NeuphonicTTSService` to work with the updated websocket API.
|
||||||
|
|
||||||
|
- Fixed an issue with `RivaSTTService` where the watchdog feature was causing
|
||||||
|
an error on initialization.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Remove unncessary push task in each `FrameProcessor`.
|
||||||
|
|
||||||
|
## [0.0.74] - 2025-07-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a new STT service, `SpeechmaticsSTTService`. This service provides
|
||||||
|
real-time speech-to-text transcription using the Speechmatics API. It supports
|
||||||
|
partial and final transcriptions, multiple languages, various audio formats,
|
||||||
|
and speaker diarization.
|
||||||
|
|
||||||
|
- Added `normalize` and `model_id` to `FishAudioTTSService`.
|
||||||
|
|
||||||
|
- Added `http_options` argument to `GoogleLLMService`.
|
||||||
|
|
||||||
|
- Added `run_llm` field to `LLMMessagesAppendFrame` and `LLMMessagesUpdateFrame`
|
||||||
|
frames. If true, a context frame will be pushed triggering the LLM to respond.
|
||||||
|
|
||||||
|
- Added a new `SOXRStreamAudioResampler` for processing audio in chunks or
|
||||||
|
streams. If you write your own processor and need to use an audio resampler,
|
||||||
|
use the new `create_stream_resampler()`.
|
||||||
|
|
||||||
|
- Added new `DailyParams.audio_in_user_tracks` to allow receiving one track per
|
||||||
|
user (default) or a single track from the room (all participants mixed).
|
||||||
|
|
||||||
|
- Added support for providing "direct" functions, which don't need an
|
||||||
|
accompanying `FunctionSchema` or function definition dict. Instead, metadata
|
||||||
|
(i.e. `name`, `description`, `properties`, and `required`) are automatically
|
||||||
|
extracted from a combination of the function signature and docstring.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# "Direct" function
|
||||||
|
# `params` must be the first parameter
|
||||||
|
async def do_something(params: FunctionCallParams, foo: int, bar: str = ""):
|
||||||
|
"""
|
||||||
|
Do something interesting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
foo (int): The foo to do something interesting with.
|
||||||
|
bar (string): The bar to do something interesting with.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = await process(foo, bar)
|
||||||
|
await params.result_callback({"result": result})
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
llm.register_direct_function(do_something)
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
tools = ToolsSchema(standard_tools=[do_something])
|
||||||
|
```
|
||||||
|
|
||||||
|
- `user_id` is now populated in the `TranscriptionFrame` and
|
||||||
|
`InterimTranscriptionFrame` when using a transport that provides a `user_id`,
|
||||||
|
like `DailyTransport` or `LiveKitTransport`.
|
||||||
|
|
||||||
|
- Added `watchdog_coroutine()`. This is a watchdog helper for couroutines. So,
|
||||||
|
if you have a coroutine that is waiting for a result and that takes a long
|
||||||
|
time, you will need to wrap it with `watchdog_coroutine()` so the watchdog
|
||||||
|
timers are reset regularly.
|
||||||
|
|
||||||
|
- Added `session_token` parameter to `AWSNovaSonicLLMService`.
|
||||||
|
|
||||||
|
- Added Gemini Multimodal Live File API for uploading, fetching, listing, and
|
||||||
|
deleting files. See `26f-gemini-multimodal-live-files-api.py` for example usage.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated all the services to use the new `SOXRStreamAudioResampler`, ensuring smooth
|
||||||
|
transitions and eliminating clicks.
|
||||||
|
|
||||||
|
- Upgraded `daily-python` to 0.19.4.
|
||||||
|
|
||||||
|
- Updated `google` optional dependency to use `google-genai` version `1.24.0`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue where audio would get stuck in the queue when an interrupt occurs
|
||||||
|
during Azure TTS synthesis.
|
||||||
|
|
||||||
|
- Fixed a race condition that occurs in Python 3.10+ where the task could miss
|
||||||
|
the `CancelledError` and continue running indefinitely, freezing the pipeline.
|
||||||
|
|
||||||
|
- Fixed a `AWSNovaSonicLLMService` issue introduced in 0.0.72.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- In `FishAudioTTSService`, deprecated `model` and replaced with
|
||||||
|
`reference_id`. This change is to better align with Fish Audio's variable
|
||||||
|
naming and to reduce confusion about what functionality the variable
|
||||||
|
controls.
|
||||||
|
|
||||||
|
## [0.0.73] - 2025-06-26
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue introduced in 0.0.72 that would cause `ElevenLabsTTSService`,
|
||||||
|
`GladiaSTTService`, `NeuphonicTTSService` and `OpenAIRealtimeBetaLLMService`
|
||||||
|
to throw an error.
|
||||||
|
|
||||||
|
## [0.0.72] - 2025-06-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added logging and improved error handling to help diagnose and prevent potential
|
||||||
|
Pipeline freezes.
|
||||||
|
|
||||||
|
- Added `WatchdogQueue`, `WatchdogPriorityQueue`, `WatchdogEvent` and
|
||||||
|
`WatchdogAsyncIterator`. These helper utilities reset watchdog timers
|
||||||
|
appropriately before they expire. When watchdog timers are disabled, the
|
||||||
|
utilities behave as standard counterparts without side effects.
|
||||||
|
|
||||||
|
- Introduce task watchdog timers. Watchdog timers are used to detect if a
|
||||||
|
Pipecat task is taking longer than expected (by default 5 seconds). Watchdog
|
||||||
|
timers are disabled by default and can be enabled globally by passing
|
||||||
|
`enable_watchdog_timers` argument to `PipelineTask` constructor. It is
|
||||||
|
possible to change the default watchdog timer timeout by using the
|
||||||
|
`watchdog_timeout` argument. You can also log how long it takes to reset the
|
||||||
|
watchdog timers which is done with the `enable_watchdog_logging`. You can
|
||||||
|
control all these settings per each frame processor or even per task. That is,
|
||||||
|
you can set `enable_watchdog_timers`, `enable_watchdog_logging` and
|
||||||
|
`watchdog_timeout` when creating any frame processor through their constructor
|
||||||
|
arguments or when you create a task with `FrameProcessor.create_task()`. Note
|
||||||
|
that watchdog timers only work with Pipecat tasks and will not work if you use
|
||||||
|
`asycio.create_task()` or similar.
|
||||||
|
|
||||||
|
- Added `lexicon_names` parameter to `AWSPollyTTSService.InputParams`.
|
||||||
|
|
||||||
|
- Added reconnection logic and audio buffer management to `GladiaSTTService`.
|
||||||
|
|
||||||
|
- The `TurnTrackingObserver` now ends a turn upon observing an `EndFrame` or
|
||||||
|
`CancelFrame`.
|
||||||
|
|
||||||
|
- Added Polish support to `AWSTranscribeSTTService`.
|
||||||
|
|
||||||
|
- Added new frames `FrameProcessorPauseFrame` and `FrameProcessorResumeFrame`
|
||||||
|
which allow pausing and resuming frame processing for a given frame
|
||||||
|
processor. These are control frames, so they are ordered. Pausing frame
|
||||||
|
processor will keep old frames in the internal queues until resume takes
|
||||||
|
place. Frames being pushed while a frame processor is paused will be pushed to
|
||||||
|
the queues. When frame processing is resumed all queued frames will be
|
||||||
|
processed in order. Also added `FrameProcessorPauseUrgentFrame` and
|
||||||
|
`FrameProcessorResumeUrgentFrame` which are system frames and therefore they
|
||||||
|
have high priority.
|
||||||
|
|
||||||
|
- Added a property called `has_function_calls_in_progress` in
|
||||||
|
`LLMAssistantContextAggregator` that exposes whether a function call is in
|
||||||
|
progress.
|
||||||
|
|
||||||
|
- Added `SambaNovaLLMService` which provides llm api integration with an
|
||||||
|
OpenAI-compatible interface.
|
||||||
|
|
||||||
|
- Added `SambaNovaTTSService` which provides speech-to-text functionality using
|
||||||
|
SambaNovas's (whisper) API.
|
||||||
|
|
||||||
|
- Add fundational examples for function calling and transcription
|
||||||
|
`14s-function-calling-sambanova.py`, `13g-sambanova-transcription.py`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `HeartbeatFrame`s are now control frames. This will make it easier to detect
|
||||||
|
pipeline freezes. Previously, heartbeat frames were system frames which meant
|
||||||
|
they were not get queued with other frames, making it difficult to detect
|
||||||
|
pipeline stalls.
|
||||||
|
|
||||||
|
- Updated `OpenAIRealtimeBetaLLMService` to accept `language` in the
|
||||||
|
`InputAudioTranscription` class for all models.
|
||||||
|
|
||||||
|
- Updated the default model for `OpenAIRealtimeBetaLLMService` to
|
||||||
|
`gpt-4o-realtime-preview-2025-06-03`.
|
||||||
|
|
||||||
|
- The `PipelineParams` arg `allow_interruptions` now defaults to `True`.
|
||||||
|
|
||||||
|
- `TavusTransport` and `TavusVideoService` now send audio to Tavus using WebRTC
|
||||||
|
audio tracks instead of `app-messages` over WebSocket. This should improve the
|
||||||
|
overall audio quality.
|
||||||
|
|
||||||
|
- Upgraded `daily-python` to 0.19.3.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue that would cause heartbeat frames to be sent before processors
|
||||||
|
were started.
|
||||||
|
|
||||||
|
- Fixed an event loop blocking issue when using `SentryMetrics`.
|
||||||
|
|
||||||
|
- Fixed an issue in `FastAPIWebsocketClient` to ensure proper disconnection
|
||||||
|
when the websocket is already closed.
|
||||||
|
|
||||||
|
- Fixed an issue where the `UserStoppedSpeakingFrame` was not received if the
|
||||||
|
transport was not receiving new audio frames.
|
||||||
|
|
||||||
|
- Fixed an edge case where if the user interrupted the bot but no new aggregation
|
||||||
|
was received, the bot would not resume speaking.
|
||||||
|
|
||||||
|
- Fixed an issue with `TelnyxFrameSerializer` where it would throw an exception
|
||||||
|
when the user hung up the call.
|
||||||
|
|
||||||
|
- Fixed an issue with `ElevenLabsTTSService` where the context was not being
|
||||||
|
closed.
|
||||||
|
|
||||||
|
- Fixed function calling in `AWSNovaSonicLLMService`.
|
||||||
|
|
||||||
|
- Fixed an issue that would cause multiple `PipelineTask.on_idle_timeout`
|
||||||
|
events to be triggered repeatedly.
|
||||||
|
|
||||||
|
- Fixed an issue that was causing user and bot speech to not be synchronized
|
||||||
|
during recordings.
|
||||||
|
|
||||||
|
- Fixed an issue where voice settings weren't applied to ElevenLabsTTSService.
|
||||||
|
|
||||||
|
- Fixed an issue with `GroqTTSService` where it was not properly parsing the
|
||||||
|
WAV file header.
|
||||||
|
|
||||||
|
- Fixed an issue with `GoogleSTTService` where it was constantly reconnecting
|
||||||
|
before starting to receive audio from the user.
|
||||||
|
|
||||||
|
- Fixed an issue where `GoogleLLMService`'s TTFB value was incorrect.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- `AudioBufferProcessor` parameter `user_continuos_stream` is deprecated.
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- Rename `14e-function-calling-gemini.py` to `14e-function-calling-google.py`.
|
||||||
|
|
||||||
|
## [0.0.71] - 2025-06-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Adds a parameter called `additional_span_attributes` to PipelineTask that
|
||||||
|
lets you add any additional attributes you'd like to the conversation span.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with `CartesiaSTTService` initialization.
|
||||||
|
|
||||||
|
## [0.0.70] - 2025-06-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `ExotelFrameSerializer` to handle telephony calls via Exotel.
|
||||||
|
|
||||||
|
- Added the option `informal` to `TranslationConfig` on Gladia config.
|
||||||
|
Allowing to force informal language forms when available.
|
||||||
|
|
||||||
|
- Added `CartesiaSTTService` which is a websocket based implementation to
|
||||||
|
transcribe audio. Added a foundational example in
|
||||||
|
`13f-cartesia-transcription.py`
|
||||||
|
|
||||||
|
- Added an `websocket` example, showing how to use the new Pipecat client
|
||||||
|
`WebsocketTransport` to connect with Pipecat `FastAPIWebsocketTransport` or
|
||||||
|
`WebsocketServerTransport`.
|
||||||
|
|
||||||
|
- Added language support to `RimeHttpTTSService`. Extended languages to include
|
||||||
|
German and French for both `RimeTTSService` and `RimeHttpTTSService`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `daily-python` to 0.19.2.
|
||||||
|
|
||||||
|
- Make `PipelineTask.add_observer()` synchronous. This allows callers to call it
|
||||||
|
before doing the work of running the `PipelineTask` (i.e. without invoking
|
||||||
|
`PipelineTask.set_event_loop()` first).
|
||||||
|
|
||||||
|
- Pipecat 0.0.69 forced `uvloop` event loop on Linux on macOS. Unfortunately,
|
||||||
|
this is causing issue in some systems. So, `uvloop` is not enabled by default
|
||||||
|
anymore. If you want to use `uvloop` you can just set the `asyncio` event
|
||||||
|
policy before starting your agent with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with various TTS services that would cause audio glitches at
|
||||||
|
the start of every bot turn.
|
||||||
|
|
||||||
|
- Fixed an `ElevenLabsTTSService` issue where a context warning was printed
|
||||||
|
when pushing a `TTSSpeakFrame`.
|
||||||
|
|
||||||
|
- Fixed an `AssemblyAISTTService` issue that could cause unexpected behavior
|
||||||
|
when yielding empty `Frame()`s.
|
||||||
|
|
||||||
|
- Fixed an issue where `OutputAudioRawFrame.transport_destination` was being
|
||||||
|
reset to `None` instead of retaining its intended value before sending the
|
||||||
|
audio frame to `write_audio_frame`.
|
||||||
|
|
||||||
|
- Fixed a typo in Livekit transport that prevented initialization.
|
||||||
|
|
||||||
|
## [0.0.69] - 2025-06-02 "AI Engineer World's Fair release" ✨
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a new frame `FunctionCallsStartedFrame`. This frame is pushed both
|
||||||
|
upstream and downstream from the LLM service to indicate that one or more
|
||||||
|
function calls are going to be executed.
|
||||||
|
|
||||||
|
- Added LLM services `on_function_calls_started` event. This event will be
|
||||||
|
triggered when the LLM service receives function calls from the model and is
|
||||||
|
going to start executing them.
|
||||||
|
|
||||||
|
- Function calls can now be executed sequentially (in the order received in the
|
||||||
|
completion) by passing `run_in_parallel=False` when creating your LLM
|
||||||
|
service. By default, if the LLM completion returns 2 or more function calls
|
||||||
|
they run concurrently. In both cases, concurrently and sequentially, a new LLM
|
||||||
|
completion will run when the last function call finishes.
|
||||||
|
|
||||||
|
- Added OpenTelemetry tracing for `GeminiMultimodalLiveLLMService` and
|
||||||
|
`OpenAIRealtimeBetaLLMService`.
|
||||||
|
|
||||||
|
- Added initial support for interruption strategies, which determine if the user
|
||||||
|
should interrupt the bot while the bot is speaking. Interruption strategies
|
||||||
|
can be based on factors such as audio volume or the number of words spoken by
|
||||||
|
the user. These can be specified via the new `interruption_strategies` field
|
||||||
|
in `PipelineParams`. A new `MinWordsInterruptionStrategy` strategy has been
|
||||||
|
introduced which triggers an interruption if the user has spoken a minimum
|
||||||
|
number of words. If no interruption strategies are specified, the normal
|
||||||
|
interruption behavior applies. If multiple strategies are provided, the first
|
||||||
|
one that evaluates to true will trigger the interruption.
|
||||||
|
|
||||||
|
- `BaseInputTransport` now handles `StopFrame`. When a `StopFrame` is received
|
||||||
|
the transport will pause sending frames downstream until a new `StartFrame` is
|
||||||
|
received. This allows the transport to be reused (keeping the same connection)
|
||||||
|
in a different pipeline.
|
||||||
|
|
||||||
|
- Updated AssemblyAI STT service to support their latest streaming
|
||||||
|
speech-to-text model with improved transcription latency and endpointing.
|
||||||
|
|
||||||
|
- You can now access STT service results through the new
|
||||||
|
`TranscriptionFrame.result` and `InterimTranscriptionFrame.result` field. This
|
||||||
|
is useful in case you use some specific settings for the STT and you want to
|
||||||
|
access the STT results.
|
||||||
|
|
||||||
|
- The examples runner is now public from the `pipecat.examples` package. This
|
||||||
|
allows everyone to build their own examples and run them easily.
|
||||||
|
|
||||||
|
- It is now possible to push `OutputDTMFFrame` or `OutputDTMFUrgentFrame` with
|
||||||
|
`DailyTransport`. This will be sent properly if a Daily dial-out connection
|
||||||
|
has been established.
|
||||||
|
|
||||||
|
- Added `OutputDTMFUrgentFrame` to send a DTMF keypress quickly. The previous
|
||||||
|
`OutputDTMFFrame` queues the keypress with the rest of data frames.
|
||||||
|
|
||||||
|
- Added `DTMFAggregator`, which aggregates keypad presses into
|
||||||
|
`TranscriptionFrame`s. Aggregation occurs after a timeout, termination key
|
||||||
|
press, or user interruption. You can specify the prefix of the
|
||||||
|
`TranscriptionFrame`.
|
||||||
|
|
||||||
|
- Added new functions `DailyTransport.start_transcription()` and
|
||||||
|
`DailyTransport.stop_transcription()` to be able to start and stop Daily
|
||||||
|
transcription dynamically (maybe with different settings).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reverted the default model for `GeminiMultimodalLiveLLMService` back to
|
||||||
|
`models/gemini-2.0-flash-live-001`.
|
||||||
|
`gemini-2.5-flash-preview-native-audio-dialog` has inconsistent performance.
|
||||||
|
You can opt in to using this model by setting the `model` arg.
|
||||||
|
|
||||||
|
- Function calls are now cancelled by default if there's an interruption. To
|
||||||
|
disable this behavior you can set `cancel_on_interruption=False` when
|
||||||
|
registering the function call. Since function calls are executed as tasks you
|
||||||
|
can tell if a function call has been cancelled by catching the
|
||||||
|
`asyncio.CancelledError` exception (and don't forget to raise it again!).
|
||||||
|
|
||||||
|
- Updated OpenTelemetry tracing attribute `metrics.ttfb_ms` to `metrics.ttfb`.
|
||||||
|
The attribute reports TTFB in seconds.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- `DailyTransport.send_dtmf()` is deprecated, push an `OutputDTMFFrame` or an
|
||||||
|
`OutputDTMFUrgentFrame` instead.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with `ElevenLabsTTSService` where long responses would
|
||||||
|
continue generating output even after an interruption.
|
||||||
|
|
||||||
|
- Fixed an issue with the `OpenAILLMContext` where non-Roman characters were
|
||||||
|
being incorrectly encoded as Unicode escape sequences. This was a logging
|
||||||
|
issue and did not impact the actual conversation.
|
||||||
|
|
||||||
|
- In `AWSBedrockLLMService`, worked around a possible bug in AWS Bedrock where
|
||||||
|
a `toolConfig` is required if there has been previous tool use in the
|
||||||
|
messages array. This workaround includes a no_op factory function call is
|
||||||
|
used to satisfy the requirement.
|
||||||
|
|
||||||
|
- Fixed `WebsocketClientTransport` to use `FrameProcessorSetup.task_manager`
|
||||||
|
instead of `StartFrame.task_manager`.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Use `uvloop` as the new event loop on Linux and macOS systems.
|
||||||
|
|
||||||
|
## [0.0.68] - 2025-05-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `GoogleHttpTTSService` which uses Google's HTTP TTS API.
|
||||||
|
|
||||||
|
- Added `TavusTransport`, a new transport implementation compatible with any
|
||||||
|
Pipecat pipeline. When using the `TavusTransport`the Pipecat bot will
|
||||||
|
connect in the same room as the Tavus Avatar and the user.
|
||||||
|
|
||||||
|
- Added `PlivoFrameSerializer` to support Plivo calls. A full running example
|
||||||
|
has also been added to `examples/plivo-chatbot`.
|
||||||
|
|
||||||
|
- Added `UserBotLatencyLogObserver`. This is an observer that logs the latency
|
||||||
|
between when the user stops speaking and when the bot starts speaking. This
|
||||||
|
gives you an initial idea on how quickly the AI services respond.
|
||||||
|
|
||||||
|
- Added `SarvamTTSService`, which implements Sarvam AI's TTS API:
|
||||||
|
https://docs.sarvam.ai/api-reference-docs/text-to-speech/convert.
|
||||||
|
|
||||||
|
- Added `PipelineTask.add_observer()` and `PipelineTask.remove_observer()` to
|
||||||
|
allow mangaging observers at runtime. This is useful for cases where the task
|
||||||
|
is passed around to other code components that might want to observe the
|
||||||
|
pipeline dynamically.
|
||||||
|
|
||||||
|
- Added `user_id` field to `TranscriptionMessage`. This allows identifying the
|
||||||
|
user in a multi-user scenario. Note that this requires that
|
||||||
|
`TranscriptionFrame` has the `user_id` properly set.
|
||||||
|
|
||||||
|
- Added new `PipelineTask` event handlers `on_pipeline_started`,
|
||||||
|
`on_pipeline_stopped`, `on_pipeline_ended` and `on_pipeline_cancelled`, which
|
||||||
|
correspond to the `StartFrame`, `StopFrame`, `EndFrame` and `CancelFrame`
|
||||||
|
respectively.
|
||||||
|
|
||||||
|
- Added additional languages to `LmntTTSService`. Languages include: `hi`,
|
||||||
|
`id`, `it`, `ja`, `nl`, `pl`, `ru`, `sv`, `th`, `tr`, `uk`, `vi`.
|
||||||
|
|
||||||
|
- Added a `model` parameter to the `LmntTTSService` constructor, allowing
|
||||||
|
switching between LMNT models.
|
||||||
|
|
||||||
|
- Added `MiniMaxHttpTTSService`, which implements MiniMax's T2A API for TTS.
|
||||||
|
Learn more: https://www.minimax.io/platform_overview
|
||||||
|
|
||||||
|
- A new function `FrameProcessor.setup()` has been added to allow setting up
|
||||||
|
frame processors before receiving a `StartFrame`. This is what's happening
|
||||||
|
internally: `FrameProcessor.setup()` is called, `StartFrame` is pushed from
|
||||||
|
the beginning of the pipeline, your regular pipeline operations, `EndFrame`
|
||||||
|
or `CancelFrame` are pushed from the beginning of the pipeline and finally
|
||||||
|
`FrameProcessor.cleanup()` is called.
|
||||||
|
|
||||||
|
- Added support for OpenTelemetry tracing in Pipecat. This initial
|
||||||
|
implementation includes:
|
||||||
|
|
||||||
|
- A `setup_tracing` method where you can specify your OpenTelemetry exporter
|
||||||
|
- Service decorators for STT (`@traced_stt`), LLM (`@traced_llm`), and TTS
|
||||||
|
(`@traced_tts`) which trace the execution and collect properties and
|
||||||
|
metrics (TTFB, token usage, character counts, etc.)
|
||||||
|
- Class decorators that provide execution tracking; these are generic and can
|
||||||
|
be used for service tracking as needed
|
||||||
|
- Spans that help track traces on a per conversations and turn basis:
|
||||||
|
|
||||||
|
```
|
||||||
|
conversation-uuid
|
||||||
|
├── turn-1
|
||||||
|
│ ├── stt_deepgramsttservice
|
||||||
|
│ ├── llm_openaillmservice
|
||||||
|
│ └── tts_cartesiattsservice
|
||||||
|
...
|
||||||
|
└── turn-n
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, Pipecat has implemented service decorators to trace execution of
|
||||||
|
STT, LLM, and TTS services. You can enable tracing by setting
|
||||||
|
`enable_tracing` to `True` in the PipelineTask.
|
||||||
|
|
||||||
|
- Added `TurnTrackingObserver`, which tracks the start and end of a user/bot
|
||||||
|
turn pair and emits events `on_turn_started` and `on_turn_stopped`
|
||||||
|
corresponding to the start and end of a turn, respectively.
|
||||||
|
|
||||||
|
- Allow passing observers to `run_test()` while running unit tests.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `daily-python` to 0.19.1.
|
||||||
|
|
||||||
|
- ⚠️ Updated `SmallWebRTCTransport` to align with how other transports handle
|
||||||
|
`on_client_disconnected`. Now, when the connection is closed and no reconnection
|
||||||
|
is attempted, `on_client_disconnected` is called instead of `on_client_close`. The
|
||||||
|
`on_client_close` callback is no longer used, use `on_client_disconnected` instead.
|
||||||
|
|
||||||
|
- Check if `PipelineTask` has already been cancelled.
|
||||||
|
|
||||||
|
- Don't raise an exception if event handler is not registered.
|
||||||
|
|
||||||
|
- Upgraded `deepgram-sdk` to 4.1.0.
|
||||||
|
|
||||||
|
- Updated `GoogleTTSService` to use Google's streaming TTS API. The default
|
||||||
|
voice also updated to `en-US-Chirp3-HD-Charon`.
|
||||||
|
|
||||||
|
- ⚠️ Refactored the `TavusVideoService`, so it acts like a proxy, sending audio
|
||||||
|
to Tavus and receiving both audio and video. This will make
|
||||||
|
`TavusVideoService` usable with any Pipecat pipeline and with any transport.
|
||||||
|
This is a **breaking change**, check the
|
||||||
|
`examples/foundational/21a-tavus-layer-small-webrtc.py` to see how to use it.
|
||||||
|
|
||||||
|
- `DailyTransport` now uses custom microphone audio tracks instead of virtual
|
||||||
|
microphones. Now, multiple Daily transports can be used in the same process.
|
||||||
|
|
||||||
|
- `DailyTransport` now captures audio from individual participants instead of
|
||||||
|
the whole room. This allows identifying audio frames per participant.
|
||||||
|
|
||||||
|
- Updated the default model for `AnthropicLLMService` to
|
||||||
|
`claude-sonnet-4-20250514`.
|
||||||
|
|
||||||
|
- Updated the default model for `GeminiMultimodalLiveLLMService` to
|
||||||
|
`models/gemini-2.5-flash-preview-native-audio-dialog`.
|
||||||
|
|
||||||
|
- `BaseTextFilter` methods `filter()`, `update_settings()`,
|
||||||
|
`handle_interruption()` and `reset_interruption()` are now async.
|
||||||
|
|
||||||
|
- `BaseTextAggregator` methods `aggregate()`, `handle_interruption()` and
|
||||||
|
`reset()` are now async.
|
||||||
|
|
||||||
|
- The API version for `CartesiaTTSService` and `CartesiaHttpTTSService` has
|
||||||
|
been updated. Also, the `cartesia` dependency has been updated to 2.x.
|
||||||
|
|
||||||
|
- `CartesiaTTSService` and `CartesiaHttpTTSService` now support Cartesia's new
|
||||||
|
`speed` parameter which accepts values of `slow`, `normal`, and `fast`.
|
||||||
|
|
||||||
|
- `GeminiMultimodalLiveLLMService` now uses the user transcription and usage
|
||||||
|
metrics provided by Gemini Live.
|
||||||
|
|
||||||
|
- `GoogleLLMService` has been updated to use `google-genai` instead of the
|
||||||
|
deprecated `google-generativeai`.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- In `CartesiaTTSService` and `CartesiaHttpTTSService`, `emotion` has been
|
||||||
|
deprecated by Cartesia. Pipecat is following suit and deprecating `emotion`
|
||||||
|
as well.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Since `GeminiMultimodalLiveLLMService` now transcribes it's own audio, the
|
||||||
|
`transcribe_user_audio` arg has been removed. Audio is now transcribed
|
||||||
|
automatically.
|
||||||
|
|
||||||
|
- Removed `SileroVAD` frame processor, just use `SileroVADAnalyzer`
|
||||||
|
instead. Also removed, `07a-interruptible-vad.py` example.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a `DailyTransport` issue that was not allow capturing video frames if
|
||||||
|
framerate was greater than zero.
|
||||||
|
|
||||||
|
- Fixed a `DeegramSTTService` connection issue when the user provided their own
|
||||||
|
`LiveOptions`.
|
||||||
|
|
||||||
|
- Fixed a `DailyTransport` issue that would cause images needing resize to block
|
||||||
|
the event loop.
|
||||||
|
|
||||||
|
- Fixed an issue with `ElevenLabsTTSService` where changing the model or voice
|
||||||
|
while the service is running wasn't working.
|
||||||
|
|
||||||
|
- Fixed an issue that would cause multiple instances of the same class to behave
|
||||||
|
incorrectly if any of the given constructor arguments defaulted to a mutable
|
||||||
|
value (e.g. lists, dictionaries, objects).
|
||||||
|
|
||||||
|
- Fixed an issue with `CartesiaTTSService` where `TTSTextFrame` messages weren't
|
||||||
|
being emitted when the model was set to `sonic`. This resulted in the
|
||||||
|
assistant context not being updated with assistant messages.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- `DailyTransport`: process audio, video and events in separate tasks.
|
||||||
|
|
||||||
|
- Don't create event handler tasks if no user event handlers have been
|
||||||
|
registered.
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- It is now possible to run all (or most) foundational example with multiple
|
||||||
|
transports. By default, they run with P2P (Peer-To-Peer) WebRTC so you can try
|
||||||
|
everything locally. You can also run them with Daily or even with a Twilio
|
||||||
|
phone number.
|
||||||
|
|
||||||
|
- Added foundation examples `07y-interruptible-minimax.py` and
|
||||||
|
`07z-interruptible-sarvam.py`to show how to use the `MiniMaxHttpTTSService`
|
||||||
|
and `SarvamTTSService`, respectively.
|
||||||
|
|
||||||
|
- Added an `open-telemetry-tracing` example, showing how to setup tracing. The
|
||||||
|
example also includes Jaeger as an open source OpenTelemetry client to review
|
||||||
|
traces from the example runs.
|
||||||
|
|
||||||
|
- Added foundational example `29-turn-tracking-observer.py` to show how to use
|
||||||
|
the `TurnTrackingObserver`.
|
||||||
|
|
||||||
|
## [0.0.67] - 2025-05-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `DebugLogObserver` for detailed frame logging with configurable
|
||||||
|
filtering by frame type and endpoint. This observer automatically extracts
|
||||||
|
and formats all frame data fields for debug logging.
|
||||||
|
|
||||||
|
- `UserImageRequestFrame.video_source` field has been added to request an image
|
||||||
|
from the desired video source.
|
||||||
|
|
||||||
|
- Added support for the AWS Nova Sonic speech-to-speech model with the new
|
||||||
|
`AWSNovaSonicLLMService`.
|
||||||
|
See https://docs.aws.amazon.com/nova/latest/userguide/speech.html.
|
||||||
|
Note that it requires Python >= 3.12 and `pip install pipecat-ai[aws-nova-sonic]`.
|
||||||
|
|
||||||
|
- Added new AWS services `AWSBedrockLLMService` and `AWSTranscribeSTTService`.
|
||||||
|
|
||||||
|
- Added `on_active_speaker_changed` event handler to the `DailyTransport` class.
|
||||||
|
|
||||||
|
- Added `enable_ssml_parsing` and `enable_logging` to `InputParams` in
|
||||||
|
`ElevenLabsTTSService`.
|
||||||
|
|
||||||
|
- Added support to `RimeHttpTTSService` for the `arcana` model.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated `ElevenLabsTTSService` to use the beta websocket API
|
||||||
|
(multi-stream-input). This new API supports context_ids and cancelling those
|
||||||
|
contexts, which greatly improves interruption handling.
|
||||||
|
|
||||||
|
- Observers `on_push_frame()` now take a single argument `FramePushed` instead
|
||||||
|
of multiple arguments.
|
||||||
|
|
||||||
|
- Updated the default voice for `DeepgramTTSService` to `aura-2-helena-en`.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- `PollyTTSService` is now deprecated, use `AWSPollyTTSService` instead.
|
||||||
|
|
||||||
|
- Observer `on_push_frame(src, dst, frame, direction, timestamp)` is now
|
||||||
|
deprecated, use `on_push_frame(data: FramePushed)` instead.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a `DailyTransport` issue that was causing issues when multiple audio or
|
||||||
|
video sources where being captured.
|
||||||
|
|
||||||
|
- Fixed a `UltravoxSTTService` issue that would cause the service to generate
|
||||||
|
all tokens as one word.
|
||||||
|
|
||||||
|
- Fixed a `PipelineTask` issue that would cause tasks to not be cancelled if
|
||||||
|
task was cancelled from outside of Pipecat.
|
||||||
|
|
||||||
|
- Fixed a `TaskManager` that was causing dangling tasks to be reported.
|
||||||
|
|
||||||
|
- Fixed an issue that could cause data to be sent to the transports when they
|
||||||
|
were still not ready.
|
||||||
|
|
||||||
|
- Remove custom audio tracks from `DailyTransport` before leaving.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed `CanonicalMetricsService` as it's no longer maintained.
|
||||||
|
|
||||||
|
## [0.0.66] - 2025-05-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added two new input parameters to `RimeTTSService`: `pause_between_brackets`
|
||||||
|
and `phonemize_between_brackets`.
|
||||||
|
|
||||||
|
- Added support for cross-platform local smart turn detection. You can use
|
||||||
|
`LocalSmartTurnAnalyzer` for on-device inference using Torch.
|
||||||
|
|
||||||
|
- `BaseOutputTransport` now allows multiple destinations if the transport
|
||||||
|
implementation supports it (e.g. Daily's custom tracks). With multiple
|
||||||
|
destinations it is possible to send different audio or video tracks with a
|
||||||
|
single transport simultaneously. To do that, you need to set the new
|
||||||
|
`Frame.transport_destination` field with your desired transport destination
|
||||||
|
(e.g. custom track name), tell the transport you want a new destination with
|
||||||
|
`TransportParams.audio_out_destinations` or
|
||||||
|
`TransportParams.video_out_destinations` and the transport should take care of
|
||||||
|
the rest.
|
||||||
|
|
||||||
|
- Similar to the new `Frame.transport_destination`, there's a new
|
||||||
|
`Frame.transport_source` field which is set by the `BaseInputTransport` if the
|
||||||
|
incoming data comes from a non-default source (e.g. custom tracks).
|
||||||
|
|
||||||
|
- `TTSService` has a new `transport_destination` constructor parameter. This
|
||||||
|
parameter will be used to update the `Frame.transport_destination` field for
|
||||||
|
each generated `TTSAudioRawFrame`. This allows sending multiple bots' audio to
|
||||||
|
multiple destinations in the same pipeline.
|
||||||
|
|
||||||
|
- Added `DailyTransportParams.camera_out_enabled` and
|
||||||
|
`DailyTransportParams.microphone_out_enabled` which allows you to
|
||||||
|
enable/disable the main output camera or microphone tracks. This is useful if
|
||||||
|
you only want to use custom tracks and not send the main tracks. Note that you
|
||||||
|
still need `audio_out_enabled=True` or `video_out_enabled`.
|
||||||
|
|
||||||
|
- Added `DailyTransport.capture_participant_audio()` which allows you to capture
|
||||||
|
an audio source (e.g. "microphone", "screenAudio" or a custom track name) from
|
||||||
|
a remote participant.
|
||||||
|
|
||||||
|
- Added `DailyTransport.update_publishing()` which allows you to update the call
|
||||||
|
video and audio publishing settings (e.g. audio and video quality).
|
||||||
|
|
||||||
- Added `RTVIObserverParams` which allows you to configure what RTVI messages
|
- Added `RTVIObserverParams` which allows you to configure what RTVI messages
|
||||||
are sent to the clients.
|
are sent to the clients.
|
||||||
|
|
||||||
@@ -37,6 +925,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- `TransportParams.audio_mixer` now supports a string and also a dictionary to
|
||||||
|
provide a mixer per destination. For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
audio_out_mixer={
|
||||||
|
"track-1": SoundfileMixer(...),
|
||||||
|
"track-2": SoundfileMixer(...),
|
||||||
|
"track-N": SoundfileMixer(...),
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
- The `STTMuteFilter` now mutes `InterimTranscriptionFrame` and
|
- The `STTMuteFilter` now mutes `InterimTranscriptionFrame` and
|
||||||
`TranscriptionFrame` which allows the `STTMuteFilter` to be used in
|
`TranscriptionFrame` which allows the `STTMuteFilter` to be used in
|
||||||
conjunction with transports that generate transcripts, e.g. `DailyTransport`.
|
conjunction with transports that generate transcripts, e.g. `DailyTransport`.
|
||||||
@@ -70,6 +969,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
case there's no need to push audio to the rest of the pipeline, but this is
|
case there's no need to push audio to the rest of the pipeline, but this is
|
||||||
not a very common case.
|
not a very common case.
|
||||||
|
|
||||||
|
- Added `RivaSegmentedSTTService`, which allows Riva offline/batch models, such
|
||||||
|
as to be "canary-1b-asr" used in Pipecat.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
- Function calls with parameters
|
- Function calls with parameters
|
||||||
@@ -85,8 +987,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- `TransportParams.vad_audio_passthrough` parameter is now deprecated, use
|
- `TransportParams.vad_audio_passthrough` parameter is now deprecated, use
|
||||||
`TransportParams.audio_in_passthrough` instead.
|
`TransportParams.audio_in_passthrough` instead.
|
||||||
|
|
||||||
|
- `ParakeetSTTService` is now deprecated, use `RivaSTTService` instead, which uses
|
||||||
|
the model "parakeet-ctc-1.1b-asr" by default.
|
||||||
|
|
||||||
|
- `FastPitchTTSService` is now deprecated, use `RivaTTSService` instead, which uses
|
||||||
|
the model "magpie-tts-multilingual" by default.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with `SimliVideoService` where the bot was continuously outputting
|
||||||
|
audio, which prevents the `BotStoppedSpeakingFrame` from being emitted.
|
||||||
|
|
||||||
|
- Fixed an issue where `OpenAIRealtimeBetaLLMService` would add two assistant
|
||||||
|
messages to the context.
|
||||||
|
|
||||||
- Fixed an issue with `GeminiMultimodalLiveLLMService` where the context
|
- Fixed an issue with `GeminiMultimodalLiveLLMService` where the context
|
||||||
contained tokens instead of words.
|
contained tokens instead of words.
|
||||||
|
|
||||||
@@ -102,6 +1016,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Other
|
### Other
|
||||||
|
|
||||||
|
- Added `examples/daily-custom-tracks` to show how to send and receive Daily
|
||||||
|
custom tracks.
|
||||||
|
|
||||||
|
- Added `examples/daily-multi-translation` to showcase how to send multiple
|
||||||
|
simulataneous translations with the same transport.
|
||||||
|
|
||||||
- Added 04 foundational examples for client/server transports. Also, renamed
|
- Added 04 foundational examples for client/server transports. Also, renamed
|
||||||
`29-livekit-audio-chat.py` to `04b-transports-livekit.py`.
|
`29-livekit-audio-chat.py` to `04b-transports-livekit.py`.
|
||||||
|
|
||||||
|
|||||||
150
CONTRIBUTING.md
@@ -41,36 +41,150 @@ We use Ruff for code linting and formatting. Please ensure your code passes all
|
|||||||
|
|
||||||
We follow Google-style docstrings with these specific conventions:
|
We follow Google-style docstrings with these specific conventions:
|
||||||
|
|
||||||
- Class docstrings should fully document all parameters used in `__init__`
|
**Regular Classes:**
|
||||||
- We don't require separate docstrings for `__init__` methods when parameters are documented in the class docstring
|
|
||||||
- Property methods should have docstrings explaining their purpose and return value
|
|
||||||
|
|
||||||
Example of correctly documented class:
|
- Class docstring describes the class purpose and key functionality
|
||||||
|
- `__init__` method has its own docstring with complete `Args:` section documenting all parameters
|
||||||
|
- All public methods must have docstrings with `Args:` and `Returns:` sections as appropriate
|
||||||
|
|
||||||
|
**Dataclasses:**
|
||||||
|
|
||||||
|
- Class docstring describes the purpose and documents all fields in a `Parameters:` section
|
||||||
|
- No `__init__` docstring (auto-generated)
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
- Must have docstrings with `Returns:` section
|
||||||
|
|
||||||
|
**Abstract Methods:**
|
||||||
|
|
||||||
|
- Must have docstrings explaining what subclasses should implement
|
||||||
|
|
||||||
|
**`__init__.py` Files:**
|
||||||
|
|
||||||
|
- **Skip docstrings** for pure import/re-export modules
|
||||||
|
- **Add brief docstrings** for top-level packages or those with initialization logic
|
||||||
|
|
||||||
|
**Enums:**
|
||||||
|
|
||||||
|
- Class docstring describes the enumeration purpose
|
||||||
|
- Use `Parameters:` section to document each enum value and its meaning
|
||||||
|
- No `__init__` docstring (Enums don't have custom constructors)
|
||||||
|
|
||||||
|
**Code Examples in Docstrings:**
|
||||||
|
|
||||||
|
- Use `Examples:` as a section header for multiple examples
|
||||||
|
- Use descriptive text followed by double colons (`::`) for each example
|
||||||
|
- **Always include a blank line after the `::"`**
|
||||||
|
- Indent all code consistently within each block
|
||||||
|
- Separate multiple examples with blank lines for readability
|
||||||
|
|
||||||
|
**Lists and Bullets in Docstrings:**
|
||||||
|
|
||||||
|
- Use dashes (`-`) for bullet points, not asterisks (`*`)
|
||||||
|
- **Add a blank line before bullet lists** when they follow a colon
|
||||||
|
- Use section headers like "Supported features:" or "Behavior:" before lists
|
||||||
|
- For complex nested information, consider using paragraph format instead
|
||||||
|
|
||||||
|
**Deprecations:**
|
||||||
|
|
||||||
|
- Use `warnings.warn()` in code for runtime deprecation warnings
|
||||||
|
- Add `.. deprecated::` directive in docstrings for documentation visibility
|
||||||
|
- Include version information and describe current status
|
||||||
|
- Describe parameters in present tense, use directive to indicate deprecation status
|
||||||
|
|
||||||
|
#### Examples:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class MyClass:
|
# Regular class
|
||||||
"""Class description.
|
class MyService(BaseService):
|
||||||
|
"""Description of what the service does.
|
||||||
|
|
||||||
Additional details about the class.
|
Provides detailed explanation of the service's functionality,
|
||||||
|
key features, and usage patterns.
|
||||||
|
|
||||||
Args:
|
Supported features:
|
||||||
param1: Description of first parameter.
|
|
||||||
param2: Description of second parameter.
|
- Feature one with detailed explanation
|
||||||
|
- Feature two with additional context
|
||||||
|
- Feature three for advanced use cases
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, param1, param2):
|
def __init__(self, param1: str, old_param: str = None, **kwargs):
|
||||||
# No docstring required here as parameters are documented above
|
"""Initialize the service.
|
||||||
self.param1 = param1
|
|
||||||
self.param2 = param2
|
Args:
|
||||||
|
param1: Description of param1.
|
||||||
|
old_param: Controls legacy behavior.
|
||||||
|
|
||||||
|
.. deprecated:: 1.2.0
|
||||||
|
This parameter no longer has any effect and will be removed in version 2.0.
|
||||||
|
|
||||||
|
**kwargs: Additional arguments passed to parent.
|
||||||
|
"""
|
||||||
|
if old_param is not None:
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
"Parameter 'old_param' is deprecated and will be removed in version 2.0.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def some_property(self) -> str:
|
def sample_rate(self) -> int:
|
||||||
"""Get the formatted property value.
|
"""Get the current sample rate.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A string representation of the property.
|
The sample rate in Hz.
|
||||||
"""
|
"""
|
||||||
return f"Property: {self.param1}"
|
return self._sample_rate
|
||||||
|
|
||||||
|
async def process_data(self, data: str) -> bool:
|
||||||
|
"""Process the provided data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: The data to process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if processing succeeded.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Dataclass with code examples
|
||||||
|
@dataclass
|
||||||
|
class MessageFrame:
|
||||||
|
"""Frame containing messages in OpenAI format.
|
||||||
|
|
||||||
|
Supports both simple and content list message formats.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
[
|
||||||
|
{"role": "user", "content": "Hello"},
|
||||||
|
{"role": "assistant", "content": "Hi there!"}
|
||||||
|
]
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
messages: List of messages in OpenAI format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
messages: List[dict]
|
||||||
|
|
||||||
|
# Enum class
|
||||||
|
class Status(Enum):
|
||||||
|
"""Status codes for processing operations.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
PENDING: Operation is queued but not started.
|
||||||
|
RUNNING: Operation is currently in progress.
|
||||||
|
COMPLETED: Operation finished successfully.
|
||||||
|
FAILED: Operation encountered an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PENDING = "pending"
|
||||||
|
RUNNING = "running"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
FAILED = "failed"
|
||||||
```
|
```
|
||||||
|
|
||||||
# Contributor Covenant Code of Conduct
|
# Contributor Covenant Code of Conduct
|
||||||
|
|||||||
4
MANIFEST.in
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
prune docs
|
||||||
|
prune examples
|
||||||
|
prune scripts
|
||||||
|
prune tests
|
||||||
27
README.md
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
**Pipecat** is an open-source Python framework for building real-time voice and multimodal conversational agents. Orchestrate audio and video, AI services, different transports, and conversation pipelines effortlessly—so you can focus on what makes your agent unique.
|
**Pipecat** is an open-source Python framework for building real-time voice and multimodal conversational agents. Orchestrate audio and video, AI services, different transports, and conversation pipelines effortlessly—so you can focus on what makes your agent unique.
|
||||||
|
|
||||||
|
> Want to dive right in? [Install Pipecat](https://docs.pipecat.ai/getting-started/installation) then try the [quickstart](https://docs.pipecat.ai/getting-started/quickstart).
|
||||||
|
|
||||||
## 🚀 What You Can Build
|
## 🚀 What You Can Build
|
||||||
|
|
||||||
- **Voice Assistants** – natural, streaming conversations with AI
|
- **Voice Assistants** – natural, streaming conversations with AI
|
||||||
@@ -49,18 +51,19 @@ You can connect to Pipecat from any platform using our official SDKs:
|
|||||||
|
|
||||||
## 🧩 Available services
|
## 🧩 Available services
|
||||||
|
|
||||||
| Category | Services |
|
| Category | Services |
|
||||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [Parakeet (NVIDIA)](https://docs.pipecat.ai/server/services/stt/parakeet), [Ultravox](https://docs.pipecat.ai/server/services/stt/ultravox), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) |
|
| Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [AWS](https://docs.pipecat.ai/server/services/stt/aws), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Cartesia](https://docs.pipecat.ai/server/services/stt/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [Parakeet (NVIDIA)](https://docs.pipecat.ai/server/services/stt/parakeet), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova), [Soniox](https://docs.pipecat.ai/server/services/stt/soniox), [Speechmatics](https://docs.pipecat.ai/server/services/stt/speechmatics), [Ultravox](https://docs.pipecat.ai/server/services/stt/ultravox), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) |
|
||||||
| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [Together AI](https://docs.pipecat.ai/server/services/llm/together) |
|
| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova) [Together AI](https://docs.pipecat.ai/server/services/llm/together) |
|
||||||
| Text-to-Speech | [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [FastPitch (NVIDIA)](https://docs.pipecat.ai/server/services/tts/fastpitch), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [LMNT](https://docs.pipecat.ai/server/services/tts/lmnt), [Neuphonic](https://docs.pipecat.ai/server/services/tts/neuphonic), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [Piper](https://docs.pipecat.ai/server/services/tts/piper), [PlayHT](https://docs.pipecat.ai/server/services/tts/playht), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) |
|
| Text-to-Speech | [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [FastPitch (NVIDIA)](https://docs.pipecat.ai/server/services/tts/fastpitch), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [LMNT](https://docs.pipecat.ai/server/services/tts/lmnt), [MiniMax](https://docs.pipecat.ai/server/services/tts/minimax), [Neuphonic](https://docs.pipecat.ai/server/services/tts/neuphonic), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [Piper](https://docs.pipecat.ai/server/services/tts/piper), [PlayHT](https://docs.pipecat.ai/server/services/tts/playht), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [Sarvam](https://docs.pipecat.ai/server/services/tts/sarvam), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) |
|
||||||
| Speech-to-Speech | [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai) |
|
| Speech-to-Speech | [AWS Nova Sonic](https://docs.pipecat.ai/server/services/s2s/aws), [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai) |
|
||||||
| Transport | [Daily (WebRTC)](https://docs.pipecat.ai/server/services/transport/daily), [FastAPI Websocket](https://docs.pipecat.ai/server/services/transport/fastapi-websocket), [SmallWebRTCTransport](https://docs.pipecat.ai/server/services/transport/small-webrtc), [WebSocket Server](https://docs.pipecat.ai/server/services/transport/websocket-server), Local |
|
| Transport | [Daily (WebRTC)](https://docs.pipecat.ai/server/services/transport/daily), [FastAPI Websocket](https://docs.pipecat.ai/server/services/transport/fastapi-websocket), [SmallWebRTCTransport](https://docs.pipecat.ai/server/services/transport/small-webrtc), [WebSocket Server](https://docs.pipecat.ai/server/services/transport/websocket-server), Local |
|
||||||
| Video | [Tavus](https://docs.pipecat.ai/server/services/video/tavus), [Simli](https://docs.pipecat.ai/server/services/video/simli) |
|
| Serializers | [Plivo](https://docs.pipecat.ai/server/utilities/serializers/plivo), [Twilio](https://docs.pipecat.ai/server/utilities/serializers/twilio), [Telnyx](https://docs.pipecat.ai/server/utilities/serializers/telnyx) |
|
||||||
| Memory | [mem0](https://docs.pipecat.ai/server/services/memory/mem0) |
|
| Video | [Tavus](https://docs.pipecat.ai/server/services/video/tavus), [Simli](https://docs.pipecat.ai/server/services/video/simli) |
|
||||||
| Vision & Image | [fal](https://docs.pipecat.ai/server/services/image-generation/fal), [Google Imagen](https://docs.pipecat.ai/server/services/image-generation/fal), [Moondream](https://docs.pipecat.ai/server/services/vision/moondream) |
|
| Memory | [mem0](https://docs.pipecat.ai/server/services/memory/mem0) |
|
||||||
| Audio Processing | [Silero VAD](https://docs.pipecat.ai/server/utilities/audio/silero-vad-analyzer), [Krisp](https://docs.pipecat.ai/server/utilities/audio/krisp-filter), [Koala](https://docs.pipecat.ai/server/utilities/audio/koala-filter), [Noisereduce](https://docs.pipecat.ai/server/utilities/audio/noisereduce-filter) |
|
| Vision & Image | [fal](https://docs.pipecat.ai/server/services/image-generation/fal), [Google Imagen](https://docs.pipecat.ai/server/services/image-generation/fal), [Moondream](https://docs.pipecat.ai/server/services/vision/moondream) |
|
||||||
| Analytics & Metrics | [Canonical AI](https://docs.pipecat.ai/server/services/analytics/canonical), [Sentry](https://docs.pipecat.ai/server/services/analytics/sentry) |
|
| Audio Processing | [Silero VAD](https://docs.pipecat.ai/server/utilities/audio/silero-vad-analyzer), [Krisp](https://docs.pipecat.ai/server/utilities/audio/krisp-filter), [Koala](https://docs.pipecat.ai/server/utilities/audio/koala-filter), [Noisereduce](https://docs.pipecat.ai/server/utilities/audio/noisereduce-filter) |
|
||||||
|
| Analytics & Metrics | [OpenTelemetry](https://docs.pipecat.ai/server/utilities/opentelemetry), [Sentry](https://docs.pipecat.ai/server/services/analytics/sentry) |
|
||||||
|
|
||||||
📚 [View full services documentation →](https://docs.pipecat.ai/server/services/supported-services)
|
📚 [View full services documentation →](https://docs.pipecat.ai/server/services/supported-services)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
build~=1.2.2
|
build~=1.2.2
|
||||||
coverage~=7.6.12
|
coverage~=7.9.1
|
||||||
grpcio-tools~=1.67.1
|
grpcio-tools~=1.67.1
|
||||||
pip-tools~=7.4.1
|
pip-tools~=7.4.1
|
||||||
pre-commit~=4.0.1
|
pre-commit~=4.2.0
|
||||||
pyright~=1.1.397
|
pyright~=1.1.402
|
||||||
pytest~=8.3.4
|
pytest~=8.4.1
|
||||||
pytest-asyncio~=0.25.3
|
pytest-asyncio~=1.0.0
|
||||||
pytest-aiohttp==1.1.0
|
pytest-aiohttp==1.1.0
|
||||||
ruff~=0.11.1
|
ruff~=0.12.1
|
||||||
setuptools~=70.0.0
|
setuptools~=78.1.1
|
||||||
setuptools_scm~=8.1.0
|
setuptools_scm~=8.3.1
|
||||||
python-dotenv~=1.0.1
|
python-dotenv~=1.1.1
|
||||||
|
|
||||||
|
# For running examples
|
||||||
|
uvicorn
|
||||||
|
python-dotenv
|
||||||
|
fastapi
|
||||||
|
aiohttp
|
||||||
|
aiortc
|
||||||
191
docs/api/conf.py
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -13,7 +14,8 @@ sys.path.insert(0, str(project_root / "src"))
|
|||||||
|
|
||||||
# Project information
|
# Project information
|
||||||
project = "pipecat-ai"
|
project = "pipecat-ai"
|
||||||
copyright = "2024, Daily"
|
current_year = datetime.now().year
|
||||||
|
copyright = f"2024-{current_year}, Daily" if current_year > 2024 else "2024, Daily"
|
||||||
author = "Daily"
|
author = "Daily"
|
||||||
|
|
||||||
# General configuration
|
# General configuration
|
||||||
@@ -24,19 +26,20 @@ extensions = [
|
|||||||
"sphinx.ext.intersphinx",
|
"sphinx.ext.intersphinx",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
suppress_warnings = [
|
||||||
|
"autodoc.mocked_object",
|
||||||
|
]
|
||||||
|
|
||||||
# Napoleon settings
|
# Napoleon settings
|
||||||
napoleon_google_docstring = True
|
napoleon_google_docstring = True
|
||||||
napoleon_numpy_docstring = False
|
|
||||||
napoleon_include_init_with_doc = True
|
napoleon_include_init_with_doc = True
|
||||||
|
|
||||||
# AutoDoc settings
|
# AutoDoc settings
|
||||||
autodoc_default_options = {
|
autodoc_default_options = {
|
||||||
"members": True,
|
"members": True,
|
||||||
"member-order": "bysource",
|
"member-order": "bysource",
|
||||||
"special-members": "__init__",
|
"undoc-members": False,
|
||||||
"undoc-members": True,
|
"exclude-members": "__weakref__,model_config",
|
||||||
"exclude-members": "__weakref__",
|
|
||||||
"no-index": True,
|
|
||||||
"show-inheritance": True,
|
"show-inheritance": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,20 +74,16 @@ autodoc_mock_imports = [
|
|||||||
"langchain",
|
"langchain",
|
||||||
"lmnt",
|
"lmnt",
|
||||||
"noisereduce",
|
"noisereduce",
|
||||||
"openai",
|
|
||||||
"openpipe",
|
"openpipe",
|
||||||
"simli",
|
"simli",
|
||||||
"soundfile",
|
"soundfile",
|
||||||
|
"soniox",
|
||||||
"pipecat_ai_krisp",
|
"pipecat_ai_krisp",
|
||||||
"pyaudio",
|
"pyaudio",
|
||||||
"_tkinter",
|
"_tkinter",
|
||||||
"tkinter",
|
"tkinter",
|
||||||
"daily",
|
"daily",
|
||||||
"daily_python",
|
"daily_python",
|
||||||
"pydantic.BaseModel",
|
|
||||||
"pydantic.Field",
|
|
||||||
"pydantic._internal._model_construction",
|
|
||||||
"pydantic._internal._fields",
|
|
||||||
# Moondream dependencies
|
# Moondream dependencies
|
||||||
"torch",
|
"torch",
|
||||||
"transformers",
|
"transformers",
|
||||||
@@ -145,85 +144,76 @@ autodoc_mock_imports = [
|
|||||||
"transformers.AutoFeatureExtractor",
|
"transformers.AutoFeatureExtractor",
|
||||||
# Also add specific classes that are imported
|
# Also add specific classes that are imported
|
||||||
"AutoFeatureExtractor",
|
"AutoFeatureExtractor",
|
||||||
|
# Sentry dependencies
|
||||||
|
"sentry_sdk",
|
||||||
|
# AWS Nova Sonic dependencies
|
||||||
|
"aws_sdk_bedrock_runtime",
|
||||||
|
"aws_sdk_bedrock_runtime.client",
|
||||||
|
"aws_sdk_bedrock_runtime.config",
|
||||||
|
"aws_sdk_bedrock_runtime.models",
|
||||||
|
"smithy_aws_core",
|
||||||
|
"smithy_aws_core.credentials_resolvers",
|
||||||
|
"smithy_aws_core.credentials_resolvers.static",
|
||||||
|
"smithy_aws_core.identity",
|
||||||
|
"smithy_core",
|
||||||
|
"smithy_core.aio",
|
||||||
|
"smithy_core.aio.eventstream",
|
||||||
|
# MCP dependencies (you may already have these)
|
||||||
|
"mcp",
|
||||||
|
"mcp.client",
|
||||||
|
"mcp.client.session_group",
|
||||||
|
"mcp.client.sse",
|
||||||
|
"mcp.client.stdio",
|
||||||
|
"mcp.ClientSession",
|
||||||
|
"mcp.StdioServerParameters",
|
||||||
|
# gstreamer
|
||||||
|
"gi",
|
||||||
|
"gi.require_version",
|
||||||
|
"gi.repository",
|
||||||
|
# Protobuf mocks
|
||||||
|
"pipecat.frames.protobufs.frames_pb2",
|
||||||
|
"pipecat.serializers.protobuf",
|
||||||
|
"google.protobuf",
|
||||||
|
"google.protobuf.descriptor",
|
||||||
|
"google.protobuf.descriptor_pool",
|
||||||
|
"google.protobuf.runtime_version",
|
||||||
|
"google.protobuf.symbol_database",
|
||||||
|
"google.protobuf.internal.builder",
|
||||||
]
|
]
|
||||||
|
|
||||||
# HTML output settings
|
# HTML output settings
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = "sphinx_rtd_theme"
|
||||||
html_static_path = ["_static"]
|
html_static_path = ["_static"]
|
||||||
autodoc_typehints = "description"
|
autodoc_typehints = "signature" # Show type hints in the signature only, not in the docstring
|
||||||
html_show_sphinx = False
|
html_show_sphinx = False
|
||||||
|
|
||||||
|
|
||||||
def verify_modules():
|
def import_core_modules():
|
||||||
"""Verify that required modules are available."""
|
"""Import core pipecat modules for autodoc to discover."""
|
||||||
required_modules = {
|
core_modules = [
|
||||||
"services": [
|
"pipecat",
|
||||||
"assemblyai",
|
"pipecat.frames",
|
||||||
"aws",
|
"pipecat.pipeline",
|
||||||
"cartesia",
|
"pipecat.processors",
|
||||||
"deepgram",
|
"pipecat.services",
|
||||||
"google",
|
"pipecat.transports",
|
||||||
"lmnt",
|
"pipecat.audio",
|
||||||
"riva",
|
"pipecat.adapters",
|
||||||
"simli",
|
"pipecat.clocks",
|
||||||
],
|
"pipecat.metrics",
|
||||||
"serializers": ["livekit"],
|
"pipecat.observers",
|
||||||
"vad": ["silero", "vad_analyzer"],
|
"pipecat.serializers",
|
||||||
"transports": {
|
"pipecat.sync",
|
||||||
"services": ["daily", "livekit"],
|
"pipecat.transcriptions",
|
||||||
"local": ["audio", "tk"],
|
"pipecat.utils",
|
||||||
"network": ["fastapi_websocket", "websocket_server"],
|
]
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Skip importing modules that are in autodoc_mock_imports
|
for module_name in core_modules:
|
||||||
skipped_modules = set(autodoc_mock_imports)
|
try:
|
||||||
|
__import__(module_name)
|
||||||
missing = []
|
logger.info(f"Successfully imported {module_name}")
|
||||||
for category, modules in required_modules.items():
|
except ImportError as e:
|
||||||
if isinstance(modules, dict):
|
logger.warning(f"Failed to import {module_name}: {e}")
|
||||||
# Handle nested structure
|
|
||||||
for subcategory, submodules in modules.items():
|
|
||||||
for module in submodules:
|
|
||||||
# Check if module is in autodoc_mock_imports
|
|
||||||
if (
|
|
||||||
f"pipecat.{category}.{subcategory}.{module}" in skipped_modules
|
|
||||||
or module in skipped_modules
|
|
||||||
):
|
|
||||||
logger.info(
|
|
||||||
f"Skipping import of mocked module: pipecat.{category}.{subcategory}.{module}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
__import__(f"pipecat.{category}.{subcategory}.{module}")
|
|
||||||
logger.info(
|
|
||||||
f"Successfully imported pipecat.{category}.{subcategory}.{module}"
|
|
||||||
)
|
|
||||||
except (ImportError, TypeError, NameError) as e:
|
|
||||||
missing.append(f"pipecat.{category}.{subcategory}.{module}")
|
|
||||||
logger.warning(
|
|
||||||
f"Optional module not available: pipecat.{category}.{subcategory}.{module} - {str(e)}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Handle flat structure
|
|
||||||
for module in modules:
|
|
||||||
# Check if module is in autodoc_mock_imports
|
|
||||||
if f"pipecat.{category}.{module}" in skipped_modules or module in skipped_modules:
|
|
||||||
logger.info(f"Skipping import of mocked module: pipecat.{category}.{module}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
__import__(f"pipecat.{category}.{module}")
|
|
||||||
logger.info(f"Successfully imported pipecat.{category}.{module}")
|
|
||||||
except (ImportError, TypeError, NameError) as e:
|
|
||||||
missing.append(f"pipecat.{category}.{module}")
|
|
||||||
logger.warning(
|
|
||||||
f"Optional module not available: pipecat.{category}.{module} - {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if missing:
|
|
||||||
logger.warning(f"Some optional modules are not available: {missing}")
|
|
||||||
|
|
||||||
|
|
||||||
def clean_title(title: str) -> str:
|
def clean_title(title: str) -> str:
|
||||||
@@ -235,36 +225,7 @@ def clean_title(title: str) -> str:
|
|||||||
parts = title.split(".")
|
parts = title.split(".")
|
||||||
title = parts[-1]
|
title = parts[-1]
|
||||||
|
|
||||||
# Special cases for service names and common acronyms
|
return title
|
||||||
special_cases = {
|
|
||||||
"ai": "AI",
|
|
||||||
"aws": "AWS",
|
|
||||||
"api": "API",
|
|
||||||
"vad": "VAD",
|
|
||||||
"assemblyai": "AssemblyAI",
|
|
||||||
"deepgram": "Deepgram",
|
|
||||||
"elevenlabs": "ElevenLabs",
|
|
||||||
"openai": "OpenAI",
|
|
||||||
"openpipe": "OpenPipe",
|
|
||||||
"playht": "PlayHT",
|
|
||||||
"xtts": "XTTS",
|
|
||||||
"lmnt": "LMNT",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if the entire title is a special case
|
|
||||||
if title.lower() in special_cases:
|
|
||||||
return special_cases[title.lower()]
|
|
||||||
|
|
||||||
# Otherwise, capitalize each word
|
|
||||||
words = title.split("_")
|
|
||||||
cleaned_words = []
|
|
||||||
for word in words:
|
|
||||||
if word.lower() in special_cases:
|
|
||||||
cleaned_words.append(special_cases[word.lower()])
|
|
||||||
else:
|
|
||||||
cleaned_words.append(word.capitalize())
|
|
||||||
|
|
||||||
return " ".join(cleaned_words)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
def setup(app):
|
||||||
@@ -289,9 +250,8 @@ def setup(app):
|
|||||||
|
|
||||||
excludes = [
|
excludes = [
|
||||||
str(project_root / "src/pipecat/pipeline/to_be_updated"),
|
str(project_root / "src/pipecat/pipeline/to_be_updated"),
|
||||||
str(project_root / "src/pipecat/processors/gstreamer"),
|
str(project_root / "src/pipecat/examples"),
|
||||||
str(project_root / "src/pipecat/services/to_be_updated"),
|
str(project_root / "src/pipecat/tests"),
|
||||||
str(project_root / "src/pipecat/vad"), # deprecated
|
|
||||||
"**/test_*.py",
|
"**/test_*.py",
|
||||||
"**/tests/*.py",
|
"**/tests/*.py",
|
||||||
]
|
]
|
||||||
@@ -332,5 +292,4 @@ def setup(app):
|
|||||||
logger.error(f"Error generating API documentation: {e}", exc_info=True)
|
logger.error(f"Error generating API documentation: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
# Run module verification
|
import_core_modules()
|
||||||
verify_modules()
|
|
||||||
|
|||||||
@@ -1,57 +1,17 @@
|
|||||||
Pipecat API Reference Docs
|
Pipecat API Reference
|
||||||
==========================
|
=====================
|
||||||
|
|
||||||
Welcome to Pipecat's API reference documentation!
|
Welcome to the Pipecat API reference.
|
||||||
|
|
||||||
Pipecat is an open source framework for building voice and multimodal assistants.
|
Use the navigation on the left to browse modules, or search using the search box.
|
||||||
It provides a flexible pipeline architecture for connecting various AI services,
|
|
||||||
audio processing, and transport layers.
|
**New to Pipecat?** Check out the `main documentation <https://docs.pipecat.ai>`_ for tutorials, guides, and client SDK information.
|
||||||
|
|
||||||
Quick Links
|
Quick Links
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
* `GitHub Repository <https://github.com/pipecat-ai/pipecat>`_
|
* `GitHub Repository <https://github.com/pipecat-ai/pipecat>`_
|
||||||
* `Website <https://pipecat.ai>`_
|
* `Join our Community <https://discord.gg/pipecat>`_
|
||||||
|
|
||||||
API Reference
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Core Components
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Frames <pipecat.frames>`
|
|
||||||
* :mod:`Processors <pipecat.processors>`
|
|
||||||
* :mod:`Pipeline <pipecat.pipeline>`
|
|
||||||
|
|
||||||
Audio Processing
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Audio <pipecat.audio>`
|
|
||||||
|
|
||||||
Services
|
|
||||||
~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Services <pipecat.services>`
|
|
||||||
|
|
||||||
Transport & Serialization
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Transports <pipecat.transports>`
|
|
||||||
* :mod:`Local <pipecat.transports.local>`
|
|
||||||
* :mod:`Network <pipecat.transports.network>`
|
|
||||||
* :mod:`Services <pipecat.transports.services>`
|
|
||||||
* :mod:`Serializers <pipecat.serializers>`
|
|
||||||
|
|
||||||
Utilities
|
|
||||||
~~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Adapters <pipecat.adapters>`
|
|
||||||
* :mod:`Clocks <pipecat.clocks>`
|
|
||||||
* :mod:`Metrics <pipecat.metrics>`
|
|
||||||
* :mod:`Observers <pipecat.observers>`
|
|
||||||
* :mod:`Sync <pipecat.sync>`
|
|
||||||
* :mod:`Transcriptions <pipecat.transcriptions>`
|
|
||||||
* :mod:`Utils <pipecat.utils>`
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 3
|
:maxdepth: 3
|
||||||
@@ -71,11 +31,4 @@ Utilities
|
|||||||
Sync <api/pipecat.sync>
|
Sync <api/pipecat.sync>
|
||||||
Transcriptions <api/pipecat.transcriptions>
|
Transcriptions <api/pipecat.transcriptions>
|
||||||
Transports <api/pipecat.transports>
|
Transports <api/pipecat.transports>
|
||||||
Utils <api/pipecat.utils>
|
Utils <api/pipecat.utils>
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
@@ -10,7 +10,6 @@ pipecat-ai[anthropic]
|
|||||||
pipecat-ai[assemblyai]
|
pipecat-ai[assemblyai]
|
||||||
pipecat-ai[aws]
|
pipecat-ai[aws]
|
||||||
pipecat-ai[azure]
|
pipecat-ai[azure]
|
||||||
pipecat-ai[canonical]
|
|
||||||
pipecat-ai[cartesia]
|
pipecat-ai[cartesia]
|
||||||
pipecat-ai[cerebras]
|
pipecat-ai[cerebras]
|
||||||
pipecat-ai[deepseek]
|
pipecat-ai[deepseek]
|
||||||
@@ -43,9 +42,12 @@ pipecat-ai[openai]
|
|||||||
pipecat-ai[qwen]
|
pipecat-ai[qwen]
|
||||||
pipecat-ai[remote-smart-turn]
|
pipecat-ai[remote-smart-turn]
|
||||||
# pipecat-ai[riva] # Mocked
|
# pipecat-ai[riva] # Mocked
|
||||||
|
pipecat-ai[sambanova]
|
||||||
pipecat-ai[silero]
|
pipecat-ai[silero]
|
||||||
pipecat-ai[simli]
|
pipecat-ai[simli]
|
||||||
pipecat-ai[soundfile]
|
pipecat-ai[soundfile]
|
||||||
|
pipecat-ai[soniox]
|
||||||
|
pipecat-ai[speechmatics]
|
||||||
pipecat-ai[tavus]
|
pipecat-ai[tavus]
|
||||||
pipecat-ai[together]
|
pipecat-ai[together]
|
||||||
# pipecat-ai[ultravox] # Mocked
|
# pipecat-ai[ultravox] # Mocked
|
||||||
|
|||||||
@@ -95,9 +95,29 @@ OPENROUTER_API_KEY=...
|
|||||||
PIPER_BASE_URL=...
|
PIPER_BASE_URL=...
|
||||||
|
|
||||||
# Smart turn
|
# Smart turn
|
||||||
LOCAL_SMART_TURN_MODEL_PATH=
|
LOCAL_SMART_TURN_MODEL_PATH=...
|
||||||
FAL_SMART_TURN_API_KEY=...
|
FAL_SMART_TURN_API_KEY=...
|
||||||
|
|
||||||
# Twilio
|
# Twilio
|
||||||
TWILIO_ACCOUNT_SID=
|
TWILIO_ACCOUNT_SID=...
|
||||||
TWILIO_AUTH_TOKEN=
|
TWILIO_AUTH_TOKEN=...
|
||||||
|
|
||||||
|
# MiniMax
|
||||||
|
MINIMAX_API_KEY=...
|
||||||
|
MINIMAX_GROUP_ID=...
|
||||||
|
|
||||||
|
# Sarvam AI
|
||||||
|
SARVAM_API_KEY=...
|
||||||
|
|
||||||
|
# Soniox
|
||||||
|
SONIOX_API_KEY=
|
||||||
|
|
||||||
|
# Speechmatics
|
||||||
|
SPEECHMATICS_API_KEY=...
|
||||||
|
|
||||||
|
|
||||||
|
# SambaNova
|
||||||
|
SAMBANOVA_API_KEY=...
|
||||||
|
|
||||||
|
# Sentry
|
||||||
|
SENTRY_DSN=...
|
||||||
|
|||||||
60
examples/aws-strands/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# AWS Strands Examples
|
||||||
|
|
||||||
|
This folder contains two Python examples demonstrating how to use Pipecat with the AWS Strands agent.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
These examples show how to delegate complex, multi-step tasks to a Strands agent, which can reason step-by-step and call tools to accomplish user requests.
|
||||||
|
|
||||||
|
These examples are intentionally simplified for demonstration, using mock API calls. They work best if you ask it:
|
||||||
|
|
||||||
|
> What's the weather where the Golden Gate Bridge is?
|
||||||
|
|
||||||
|
## Example Scripts
|
||||||
|
|
||||||
|
### `black-box.py`
|
||||||
|
|
||||||
|
A minimal example that demonstrates how to use the Strands agent with Pipecat. The agent can handle multi-step queries by calling tools, but does not explain its reasoning out loud.
|
||||||
|
|
||||||
|
### `explain-thinking.py`
|
||||||
|
|
||||||
|
An enhanced example where the Strands agent explains each step of its reasoning in clear, simple language as it works through a multi-step task.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Clone the repository and navigate to this example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/pipecat-ai/pipecat.git
|
||||||
|
cd pipecat/examples/aws-strands
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set up a virtual environment:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install dependencies:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Configure environment variables:**
|
||||||
|
|
||||||
|
Copy the provided `env.example` file to `.env` and fill in the necessary credentials:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env
|
||||||
|
# Then edit .env with your preferred editor
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Run an example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python black-box.py
|
||||||
|
# or
|
||||||
|
python explain-thinking.py
|
||||||
|
```
|
||||||
206
examples/aws-strands/black-box.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
from strands import Agent, tool
|
||||||
|
from strands.models import BedrockModel
|
||||||
|
|
||||||
|
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.frames.frames import TTSSpeakFrame
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||||
|
from pipecat.services.deepgram.stt import DeepgramSTTService
|
||||||
|
from pipecat.services.llm_service import FunctionCallParams
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
|
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
||||||
|
from pipecat.transports.services.daily import DailyParams
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
"""This example demonstrates how to use the Strands agent with Pipecat.
|
||||||
|
|
||||||
|
You can delegate complex, multi-step tasks to the Strands agent, which can cycle through LLM-based reasoning and tool calls to accomplish the task.
|
||||||
|
|
||||||
|
Try asking: "What's the weather where the Golden Gate Bridge is?"
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Strands agent tools
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_location_name_from_landmark(landmark: str) -> str:
|
||||||
|
"""
|
||||||
|
Get the location name from a landmark.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
landmark (str): The name of the landmark, e.g. "Golden Gate Bridge".
|
||||||
|
"""
|
||||||
|
# Simulate fetching location
|
||||||
|
return "San Francisco, CA"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_lat_long_from_location_name(location: str) -> dict:
|
||||||
|
"""
|
||||||
|
Get the latitude and longitude for a location name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (str): The city and state, e.g. "San Francisco, CA".
|
||||||
|
"""
|
||||||
|
# Simulate fetching lat/long from a geocoding service
|
||||||
|
return {"lat": 37.7749, "long": -122.4194}
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_current_weather_from_lat_long(lat: float, long: float) -> dict:
|
||||||
|
"""
|
||||||
|
Get the current weather for a specific latitude and longitude.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lat (float): The latitude of the location.
|
||||||
|
long (float): The longitude of the location.
|
||||||
|
"""
|
||||||
|
# Simulate fetching weather data from a weather service
|
||||||
|
return {"conditions": "nice", "temperature": "75"}
|
||||||
|
|
||||||
|
|
||||||
|
# We store functions so objects (e.g. SileroVADAnalyzer) don't get
|
||||||
|
# instantiated. The function will be called when the desired transport gets
|
||||||
|
# selected.
|
||||||
|
transport_params = {
|
||||||
|
"daily": lambda: DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
"webrtc": lambda: TransportParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool):
|
||||||
|
logger.info(f"Starting bot")
|
||||||
|
|
||||||
|
strands_agent = Agent(
|
||||||
|
model=BedrockModel(
|
||||||
|
model_id="us.anthropic.claude-3-7-sonnet-20250219-v1:0", max_tokens=64000
|
||||||
|
),
|
||||||
|
tools=[
|
||||||
|
get_location_name_from_landmark,
|
||||||
|
get_lat_long_from_location_name,
|
||||||
|
get_current_weather_from_lat_long,
|
||||||
|
],
|
||||||
|
system_prompt="""
|
||||||
|
You are a helpful personal assistant who can look up information about places and weather.
|
||||||
|
|
||||||
|
Your key capabilities:
|
||||||
|
1. Look up where landmarks are located.
|
||||||
|
2. Find latitude and longitude for a location.
|
||||||
|
3. Look up the current weather for a specific latitude and longitude.
|
||||||
|
|
||||||
|
Explain each step of your reasoning in clear, simple, and concise language. Your responses will be converted to audio, so avoid special characters and numbered lists.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_location_or_weather_related_queries(params: FunctionCallParams, query: str):
|
||||||
|
"""
|
||||||
|
Handle location or weather related queries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): The user's query, e.g. "What's the weather where the Golden Gate Bridge is?".
|
||||||
|
"""
|
||||||
|
# Run in a background thread
|
||||||
|
# (Otherwise the agent blocks the event loop; one effect of that is that we don't hear
|
||||||
|
# "let me check on that" until the agent finishes)
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
result = await loop.run_in_executor(None, strands_agent, query)
|
||||||
|
await params.result_callback(result.message)
|
||||||
|
|
||||||
|
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
||||||
|
|
||||||
|
tts = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
||||||
|
)
|
||||||
|
|
||||||
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
llm.register_direct_function(handle_location_or_weather_related_queries)
|
||||||
|
|
||||||
|
@llm.event_handler("on_function_calls_started")
|
||||||
|
async def on_function_calls_started(service, function_calls):
|
||||||
|
await tts.queue_frame(TTSSpeakFrame("Let me check on that."))
|
||||||
|
|
||||||
|
tools = ToolsSchema(standard_tools=[handle_location_or_weather_related_queries])
|
||||||
|
|
||||||
|
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 so don't include special characters in your answers. Respond to what the user said in a creative and helpful way. Start by suggesting that the user ask about the weather where the Golden Gate Bridge is.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = OpenAILLMContext(messages, tools)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
stt,
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
tts,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_connected")
|
||||||
|
async def on_client_connected(transport, client):
|
||||||
|
logger.info(f"Client connected")
|
||||||
|
# Kick off the conversation.
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_disconnected")
|
||||||
|
async def on_client_disconnected(transport, client):
|
||||||
|
logger.info(f"Client disconnected")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner(handle_sigint=handle_sigint)
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from pipecat.examples.run import main
|
||||||
|
|
||||||
|
main(run_example, transport_params=transport_params)
|
||||||
8
examples/aws-strands/env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
OPENAI_API_KEY=
|
||||||
|
CARTESIA_API_KEY=
|
||||||
|
DEEPGRAM_API_KEY=
|
||||||
|
DAILY_API_KEY=
|
||||||
|
DAILY_SAMPLE_ROOM_URL=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_REGION=
|
||||||
249
examples/aws-strands/explain-thinking.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
from strands import Agent, tool
|
||||||
|
from strands.models import BedrockModel
|
||||||
|
|
||||||
|
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.frames.frames import TTSSpeakFrame
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||||
|
from pipecat.services.deepgram.stt import DeepgramSTTService
|
||||||
|
from pipecat.services.llm_service import FunctionCallParams
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
|
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
||||||
|
from pipecat.transports.services.daily import DailyParams
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
"""This example demonstrates how to use the Strands agent with Pipecat in a way where the agent explains its reasoning step-by-step.
|
||||||
|
|
||||||
|
You can delegate complex, multi-step tasks to the Strands agent, which can cycle through LLM-based reasoning and tool calls to accomplish the task.
|
||||||
|
|
||||||
|
Try asking: "What's the weather where the Golden Gate Bridge is?"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Strands agent tools
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_location_name_from_landmark(landmark: str) -> str:
|
||||||
|
"""
|
||||||
|
Get the location name from a landmark.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
landmark (str): The name of the landmark, e.g. "Golden Gate Bridge".
|
||||||
|
"""
|
||||||
|
# Simulate fetching location (slowly)
|
||||||
|
time.sleep(3)
|
||||||
|
return "San Francisco, CA"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_lat_long_from_location_name(location: str) -> dict:
|
||||||
|
"""
|
||||||
|
Get the latitude and longitude for a location name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (str): The city and state, e.g. "San Francisco, CA".
|
||||||
|
"""
|
||||||
|
# Simulate fetching lat/long from a geocoding service (slowly)
|
||||||
|
time.sleep(3)
|
||||||
|
return {"lat": 37.7749, "long": -122.4194}
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_current_weather_from_lat_long(lat: float, long: float) -> dict:
|
||||||
|
"""
|
||||||
|
Get the current weather for a specific latitude and longitude.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lat (float): The latitude of the location.
|
||||||
|
long (float): The longitude of the location.
|
||||||
|
"""
|
||||||
|
# Simulate fetching weather data from a weather service (slowly)
|
||||||
|
time.sleep(3)
|
||||||
|
return {"conditions": "nice", "temperature": "75"}
|
||||||
|
|
||||||
|
|
||||||
|
# We store functions so objects (e.g. SileroVADAnalyzer) don't get
|
||||||
|
# instantiated. The function will be called when the desired transport gets
|
||||||
|
# selected.
|
||||||
|
transport_params = {
|
||||||
|
"daily": lambda: DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
"webrtc": lambda: TransportParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool):
|
||||||
|
logger.info(f"Starting bot")
|
||||||
|
|
||||||
|
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
||||||
|
|
||||||
|
tts = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
||||||
|
)
|
||||||
|
|
||||||
|
next_strands_message_is_last = False
|
||||||
|
strands_messages_queue = asyncio.Queue()
|
||||||
|
|
||||||
|
def strands_callback_handler(**kwargs):
|
||||||
|
"""
|
||||||
|
Handle events from the Strands agent.
|
||||||
|
"""
|
||||||
|
nonlocal next_strands_message_is_last
|
||||||
|
if "event" in kwargs:
|
||||||
|
event_obj = kwargs["event"]
|
||||||
|
if event_obj and "messageStop" in event_obj:
|
||||||
|
message_stop = event_obj["messageStop"]
|
||||||
|
if message_stop and "stopReason" in message_stop:
|
||||||
|
stop_reason = message_stop["stopReason"]
|
||||||
|
if stop_reason == "end_turn":
|
||||||
|
next_strands_message_is_last = True
|
||||||
|
elif "message" in kwargs:
|
||||||
|
message_obj = kwargs["message"]
|
||||||
|
if message_obj and "content" in message_obj and "role" in message_obj:
|
||||||
|
role = message_obj["role"]
|
||||||
|
content = message_obj["content"]
|
||||||
|
if role == "assistant" and isinstance(content, list):
|
||||||
|
for content_obj in content:
|
||||||
|
if isinstance(content_obj, dict) and "text" in content_obj:
|
||||||
|
message = content_obj["text"]
|
||||||
|
if not next_strands_message_is_last:
|
||||||
|
strands_messages_queue.put_nowait(message)
|
||||||
|
|
||||||
|
async def process_strands_messages():
|
||||||
|
while True:
|
||||||
|
message = await strands_messages_queue.get()
|
||||||
|
await tts.queue_frame(TTSSpeakFrame(message))
|
||||||
|
strands_messages_queue.task_done()
|
||||||
|
|
||||||
|
asyncio.create_task(process_strands_messages())
|
||||||
|
|
||||||
|
strands_agent = Agent(
|
||||||
|
model=BedrockModel(
|
||||||
|
model_id="us.anthropic.claude-3-7-sonnet-20250219-v1:0", max_tokens=64000
|
||||||
|
),
|
||||||
|
tools=[
|
||||||
|
get_location_name_from_landmark,
|
||||||
|
get_lat_long_from_location_name,
|
||||||
|
get_current_weather_from_lat_long,
|
||||||
|
],
|
||||||
|
system_prompt="""
|
||||||
|
You are a helpful personal assistant who can look up information about places and weather.
|
||||||
|
|
||||||
|
Your key capabilities:
|
||||||
|
1. Look up where landmarks are located.
|
||||||
|
2. Find latitude and longitude for a location.
|
||||||
|
3. Look up the current weather for a specific latitude and longitude.
|
||||||
|
|
||||||
|
Explain each step of your reasoning in clear, simple, and concise language. Your responses will be converted to audio, so avoid special characters and numbered lists.
|
||||||
|
""",
|
||||||
|
callback_handler=strands_callback_handler,
|
||||||
|
)
|
||||||
|
|
||||||
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
async def handle_location_or_weather_related_queries(params: FunctionCallParams, query: str):
|
||||||
|
"""
|
||||||
|
Handle location or weather related queries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): The user's query, e.g. "What's the weather where the Golden Gate Bridge is?".
|
||||||
|
"""
|
||||||
|
# Run in a background thread
|
||||||
|
# (Otherwise the agent blocks the event loop; one effect of that is that we don't hear
|
||||||
|
# the agent's "thinking" messages until the agent finishes)
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
result = await loop.run_in_executor(None, strands_agent, query)
|
||||||
|
await params.result_callback(result.message)
|
||||||
|
|
||||||
|
llm.register_direct_function(handle_location_or_weather_related_queries)
|
||||||
|
|
||||||
|
@llm.event_handler("on_function_calls_started")
|
||||||
|
async def on_function_calls_started(service, function_calls):
|
||||||
|
await tts.queue_frame(TTSSpeakFrame("Let me check on that."))
|
||||||
|
|
||||||
|
tools = ToolsSchema(standard_tools=[handle_location_or_weather_related_queries])
|
||||||
|
|
||||||
|
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 so don't include special characters in your answers. Respond to what the user said in a creative and helpful way. Start by suggesting that the user ask about the weather where the Golden Gate Bridge is.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = OpenAILLMContext(messages, tools)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
stt,
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
tts,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_connected")
|
||||||
|
async def on_client_connected(transport, client):
|
||||||
|
logger.info(f"Client connected")
|
||||||
|
# Kick off the conversation.
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_disconnected")
|
||||||
|
async def on_client_disconnected(transport, client):
|
||||||
|
logger.info(f"Client disconnected")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner(handle_sigint=handle_sigint)
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from pipecat.examples.run import main
|
||||||
|
|
||||||
|
main(run_example, transport_params=transport_params)
|
||||||
6
examples/aws-strands/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
python-dotenv
|
||||||
|
pipecat-ai[webrtc,daily,deepgram,cartesia]
|
||||||
|
pipecat-ai-small-webrtc-prebuilt
|
||||||
|
strands-agents
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"@daily-co/daily-js": "0.74.0"
|
"@daily-co/daily-js": "0.74.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.9"
|
"vite": "^6.3.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
@@ -999,9 +999,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.3",
|
"version": "6.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||||
"integrity": "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==",
|
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.9"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@daily-co/daily-js": "0.74.0"
|
"@daily-co/daily-js": "0.74.0"
|
||||||
|
|||||||
@@ -4364,9 +4364,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -6081,9 +6081,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
# Chatbot with canonical-metrics
|
|
||||||
|
|
||||||
This project implements a chatbot using a pipeline architecture that integrates audio processing, transcription, and a language model for conversational interactions. The chatbot operates within a daily communication environment, utilizing various services for text-to-speech and language model responses.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Audio Input and Output**: Captures microphone input and plays back audio responses.
|
|
||||||
- **Voice Activity Detection**: Utilizes Silero VAD to manage audio input intelligently.
|
|
||||||
- **Text-to-Speech**: Integrates ElevenLabs TTS service to convert text responses into audio.
|
|
||||||
- **Language Model Interaction**: Uses OpenAI's GPT-4 model to generate responses based on user input.
|
|
||||||
- **Transcription Services**: Captures and transcribes participant speech for analytics.
|
|
||||||
- **Metrics Collection**: Sends audio data for analysis via Canonical Metrics Service.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Python 3.10+
|
|
||||||
- `python-dotenv`
|
|
||||||
- Additional libraries from the `pipecat` package.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
1. Clone the repository.
|
|
||||||
2. Install the required packages.
|
|
||||||
3. Set up environment variables for API keys:
|
|
||||||
- `OPENAI_API_KEY`
|
|
||||||
- `ELEVENLABS_API_KEY`
|
|
||||||
- `CANONICAL_API_KEY`
|
|
||||||
- `CANONICAL_API_URL`
|
|
||||||
4. Run the script.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
The chatbot introduces itself and engages in conversations, providing brief and creative responses. Designed for flexibility, it can support multiple languages with appropriate configuration.
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
- Participants joining or leaving the call are handled dynamically, adjusting the chatbot's behavior accordingly.
|
|
||||||
|
|
||||||
|
|
||||||
ℹ️ The first time, things might take extra time to get started since VAD (Voice Activity Detection) model needs to be downloaded.
|
|
||||||
|
|
||||||
## Get started
|
|
||||||
|
|
||||||
```python
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
cp env.example .env # and add your credentials
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run the server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, visit `http://localhost:7860/` in your browser to start a chatbot session.
|
|
||||||
|
|
||||||
## Build and test the Docker image
|
|
||||||
|
|
||||||
```
|
|
||||||
docker build -t chatbot .
|
|
||||||
docker run --env-file .env -p 7860:7860 chatbot
|
|
||||||
```
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024–2025, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from loguru import logger
|
|
||||||
from runner import configure
|
|
||||||
|
|
||||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
|
||||||
from pipecat.frames.frames import EndFrame
|
|
||||||
from pipecat.pipeline.pipeline import Pipeline
|
|
||||||
from pipecat.pipeline.runner import PipelineRunner
|
|
||||||
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
||||||
from pipecat.processors.audio.audio_buffer_processor import AudioBufferProcessor
|
|
||||||
from pipecat.services.canonical.metrics import CanonicalMetricsService
|
|
||||||
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService
|
|
||||||
from pipecat.services.openai.llm import OpenAILLMService
|
|
||||||
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
|
||||||
|
|
||||||
load_dotenv(override=True)
|
|
||||||
|
|
||||||
logger.remove(0)
|
|
||||||
logger.add(sys.stderr, level="DEBUG")
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
(room_url, token) = await configure(session)
|
|
||||||
|
|
||||||
transport = DailyTransport(
|
|
||||||
room_url,
|
|
||||||
token,
|
|
||||||
"Chatbot",
|
|
||||||
DailyParams(
|
|
||||||
audio_out_enabled=True,
|
|
||||||
audio_in_enabled=True,
|
|
||||||
video_out_enabled=False,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
transcription_enabled=True,
|
|
||||||
#
|
|
||||||
# Spanish
|
|
||||||
#
|
|
||||||
# transcription_settings=DailyTranscriptionSettings(
|
|
||||||
# language="es",
|
|
||||||
# tier="nova",
|
|
||||||
# model="2-general"
|
|
||||||
# )
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
tts = ElevenLabsTTSService(
|
|
||||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
|
||||||
#
|
|
||||||
# English
|
|
||||||
#
|
|
||||||
voice_id="cgSgspJ2msm6clMCkdW9",
|
|
||||||
#
|
|
||||||
# Spanish
|
|
||||||
#
|
|
||||||
# model="eleven_multilingual_v2",
|
|
||||||
# voice_id="gD1IexrzCvsXPHUuT0s3",
|
|
||||||
)
|
|
||||||
|
|
||||||
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
|
||||||
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
#
|
|
||||||
# English
|
|
||||||
#
|
|
||||||
"content": "You are Chatbot, a friendly, helpful robot. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way, but keep your responses brief. Start by introducing yourself. Keep all your responses to 12 words or fewer.",
|
|
||||||
#
|
|
||||||
# Spanish
|
|
||||||
#
|
|
||||||
# "content": "Eres Chatbot, un amigable y útil robot. Tu objetivo es demostrar tus capacidades de una manera breve. Tus respuestas se convertiran a audio así que nunca no debes incluir caracteres especiales. Contesta a lo que el usuario pregunte de una manera creativa, útil y breve. Empieza por presentarte a ti mismo.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
context = OpenAILLMContext(messages)
|
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
|
||||||
|
|
||||||
"""
|
|
||||||
CanonicalMetrics uses AudioBufferProcessor under the hood to buffer the audio. On
|
|
||||||
call completion, CanonicalMetrics will send the audio buffer to Canonical for
|
|
||||||
analysis. Visit https://voice.canonical.chat to learn more.
|
|
||||||
"""
|
|
||||||
audio_buffer_processor = AudioBufferProcessor(num_channels=2)
|
|
||||||
canonical = CanonicalMetricsService(
|
|
||||||
audio_buffer_processor=audio_buffer_processor,
|
|
||||||
aiohttp_session=session,
|
|
||||||
api_key=os.getenv("CANONICAL_API_KEY"),
|
|
||||||
call_id=str(uuid.uuid4()),
|
|
||||||
assistant="pipecat-chatbot",
|
|
||||||
assistant_speaks_first=True,
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
pipeline = Pipeline(
|
|
||||||
[
|
|
||||||
transport.input(), # microphone
|
|
||||||
context_aggregator.user(),
|
|
||||||
llm,
|
|
||||||
tts,
|
|
||||||
transport.output(),
|
|
||||||
canonical, # uploads audio buffer to Canonical AI for metrics
|
|
||||||
audio_buffer_processor, # captures audio into a buffer
|
|
||||||
context_aggregator.assistant(),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True))
|
|
||||||
|
|
||||||
@transport.event_handler("on_first_participant_joined")
|
|
||||||
async def on_first_participant_joined(transport, participant):
|
|
||||||
await audio_buffer_processor.start_recording()
|
|
||||||
await transport.capture_participant_transcription(participant["id"])
|
|
||||||
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
|
||||||
|
|
||||||
@transport.event_handler("on_participant_left")
|
|
||||||
async def on_participant_left(transport, participant, reason):
|
|
||||||
print(f"Participant left: {participant}")
|
|
||||||
await task.cancel()
|
|
||||||
|
|
||||||
@transport.event_handler("on_call_state_updated")
|
|
||||||
async def on_call_state_updated(transport, state):
|
|
||||||
if state == "left":
|
|
||||||
# Here we don't want to cancel, we just want to finish sending
|
|
||||||
# whatever is queued, so we use an EndFrame().
|
|
||||||
await task.queue_frame(EndFrame())
|
|
||||||
|
|
||||||
runner = PipelineRunner()
|
|
||||||
|
|
||||||
await runner.run(task)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
python-dotenv
|
|
||||||
fastapi[all]
|
|
||||||
uvicorn
|
|
||||||
pipecat-ai[daily,openai,silero,elevenlabs,canonical]
|
|
||||||
|
|
||||||
@@ -128,7 +128,15 @@ async def main():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True))
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
audio_in_sample_rate=16000,
|
||||||
|
audio_out_sample_rate=16000,
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@audiobuffer.event_handler("on_audio_data")
|
@audiobuffer.event_handler("on_audio_data")
|
||||||
async def on_audio_data(buffer, audio, sample_rate, num_channels):
|
async def on_audio_data(buffer, audio, sample_rate, num_channels):
|
||||||
|
|||||||
@@ -53,4 +53,3 @@ async def configure(aiohttp_session: aiohttp.ClientSession):
|
|||||||
token = await daily_rest_helper.get_token(url, expiry_time)
|
token = await daily_rest_helper.get_token(url, expiry_time)
|
||||||
|
|
||||||
return (url, token)
|
return (url, token)
|
||||||
return (url, token)
|
|
||||||
|
|||||||
39
examples/daily-custom-tracks/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Daily Custom Tracks
|
||||||
|
|
||||||
|
This example shows how to send and receive Daily custom tracks. We will run a simple `daily-python` application to send an audio file with a custom track (named "pipecat") to a room. Then, the Pipecat bot will mirror that custom track into another custom track (named "pipecat-mirror") in the same room.
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the bot
|
||||||
|
|
||||||
|
Start the bot by giving it a Daily room URL.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python bot.py -u ROOM_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot will wait for the first participant to join. Then, it will mirror a custom track named "pipecat" into a new custom track named "pipecat-mirror".
|
||||||
|
|
||||||
|
## Run the sender
|
||||||
|
|
||||||
|
Now, run the custom track sender. This is a simple `daily-python` application that opens and audio file and sends it as a custom track to the same Daily room.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python custom_track_sender.py -u ROOM_URL -i office-ambience-mono-16000.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open client
|
||||||
|
|
||||||
|
Finally, open the client so you can hear both custom tracks.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the client is opened, copy the URL of the Daily room and join it. You should be able to select which custom track you want to hear.
|
||||||
89
examples/daily-custom-tracks/bot.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from loguru import logger
|
||||||
|
from runner import configure
|
||||||
|
|
||||||
|
from pipecat.frames.frames import Frame, InputAudioRawFrame, OutputAudioRawFrame
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
|
||||||
|
class CustomTrackMirrorProcessor(FrameProcessor):
|
||||||
|
def __init__(self, transport_destination: str, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._transport_destination = transport_destination
|
||||||
|
|
||||||
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
if isinstance(frame, InputAudioRawFrame) and frame.transport_source:
|
||||||
|
output_frame = OutputAudioRawFrame(
|
||||||
|
audio=frame.audio,
|
||||||
|
sample_rate=frame.sample_rate,
|
||||||
|
num_channels=frame.num_channels,
|
||||||
|
)
|
||||||
|
output_frame.transport_destination = self._transport_destination
|
||||||
|
await self.push_frame(output_frame)
|
||||||
|
else:
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
(room_url, _) = await configure(session)
|
||||||
|
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
None,
|
||||||
|
"Custom tracks mirror",
|
||||||
|
DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
microphone_out_enabled=False, # Disable since we just use custom tracks
|
||||||
|
audio_out_destinations=["pipecat-mirror"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(), # Transport user input
|
||||||
|
CustomTrackMirrorProcessor("pipecat-mirror"),
|
||||||
|
transport.output(), # Transport bot output
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
audio_in_sample_rate=16000,
|
||||||
|
audio_out_sample_rate=16000,
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport.event_handler("on_first_participant_joined")
|
||||||
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
await transport.capture_participant_audio(participant["id"], audio_source="pipecat")
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
74
examples/daily-custom-tracks/custom_track_sender.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
from daily import CallClient, CustomAudioSource, Daily
|
||||||
|
from pydub import AudioSegment
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||||
|
parser.add_argument("-u", "--url", type=str, required=True, help="URL of the Daily room to join")
|
||||||
|
parser.add_argument(
|
||||||
|
"-i", "--input", type=str, required=True, help="Input audio file (needs 16000 sample rate)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
audio = AudioSegment.from_mp3(args.input)
|
||||||
|
|
||||||
|
raw_bytes = audio.raw_data
|
||||||
|
sample_rate = audio.frame_rate
|
||||||
|
channels = audio.channels
|
||||||
|
|
||||||
|
print(f"Length: {len(raw_bytes)} bytes")
|
||||||
|
print(f"Sample rate: {sample_rate}, Channels: {channels}")
|
||||||
|
|
||||||
|
# Initialize the Daily context & create call client
|
||||||
|
Daily.init()
|
||||||
|
|
||||||
|
client = CallClient()
|
||||||
|
|
||||||
|
# Join the room and indicate we have a custom track named "pipecat".
|
||||||
|
client.join(
|
||||||
|
args.url,
|
||||||
|
client_settings={
|
||||||
|
"publishing": {
|
||||||
|
"camera": False,
|
||||||
|
"microphone": False,
|
||||||
|
"customAudio": {"pipecat": True},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Just sleep for a couple of seconds. To do this well we should really use
|
||||||
|
# completions.
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Create the custom audio source. This is where we will write our audio.
|
||||||
|
audio_source = CustomAudioSource(sample_rate, channels)
|
||||||
|
|
||||||
|
# Create an audio track and assign it our audio source.
|
||||||
|
client.add_custom_audio_track("pipecat", audio_source)
|
||||||
|
|
||||||
|
# Just sleep for a second. To do this well we should really use completions.
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Just write one second of audio until we have read all the file.
|
||||||
|
chunk_size = sample_rate * channels * 2
|
||||||
|
while len(raw_bytes) > 0:
|
||||||
|
chunk = raw_bytes[:chunk_size]
|
||||||
|
raw_bytes = raw_bytes[chunk_size:]
|
||||||
|
audio_source.write_frames(chunk)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
client.leave()
|
||||||
|
|
||||||
|
# Just sleep for a second. To do this well we should really use completions.
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
client.release()
|
||||||
173
examples/daily-custom-tracks/index.html
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>daily custom tracks</title>
|
||||||
|
</head>
|
||||||
|
<script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.js"></script>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.css"
|
||||||
|
/>
|
||||||
|
<script>
|
||||||
|
function enableButton(buttonId, enable) {
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
button.disabled = !enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableJoinButton(enable) {
|
||||||
|
enableButton("join-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableLeaveButton(enable) {
|
||||||
|
enableButton("leave-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyPlayers(query) {
|
||||||
|
const items = document.querySelectorAll(query);
|
||||||
|
if (items) {
|
||||||
|
for (const item of items) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyParticipantPlayers(participantId) {
|
||||||
|
destroyPlayers(`audio[data-participant-id="${participantId}"]`);
|
||||||
|
destroyPlayers(`button[data-participant-id="${participantId}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startPlayer(player, track) {
|
||||||
|
player.muted = false;
|
||||||
|
player.autoplay = true;
|
||||||
|
if (track != null) {
|
||||||
|
player.srcObject = new MediaStream([track]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAudioPlayer(track, participantId) {
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
const player = document.createElement("audio");
|
||||||
|
player.dataset.participantId = participantId;
|
||||||
|
|
||||||
|
// Create a new button for controlling audio
|
||||||
|
const audioControlButton = document.createElement("button");
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
audioControlButton.innerText = track._mediaTag == "cam-audio" ? "english" : track._mediaTag;
|
||||||
|
audioControlButton.dataset.participantId = participantId;
|
||||||
|
audioControlButton.onclick = () => {
|
||||||
|
if (player.paused) {
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
audioControlButton.className = "ui primary red button"
|
||||||
|
} else {
|
||||||
|
player.pause();
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audioContainer.appendChild(player);
|
||||||
|
audioContainer.appendChild(audioControlButton);
|
||||||
|
|
||||||
|
await startPlayer(player, track);
|
||||||
|
player.pause()
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeToTracks(participantId) {
|
||||||
|
console.log(`subscribing to track`);
|
||||||
|
|
||||||
|
if (participantId === "local") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callObject.updateParticipant(participantId, {
|
||||||
|
setSubscribedTracks: {
|
||||||
|
audio: true,
|
||||||
|
video: false,
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDaily() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
window.callObject = window.DailyIframe.createCallObject({});
|
||||||
|
|
||||||
|
callObject.on("participant-joined", (e) => {
|
||||||
|
if (!e.participant.local) {
|
||||||
|
console.log("participant-joined", e.participant);
|
||||||
|
subscribeToTracks(e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("participant-left", (e) => {
|
||||||
|
console.log("participant-left", e.participant.session_id);
|
||||||
|
destroyParticipantPlayers(e.participant.session_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("track-started", async (e) => {
|
||||||
|
console.log("track-started", e.track);
|
||||||
|
if (e.track.kind === "audio") {
|
||||||
|
await buildAudioPlayer(e.track, e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinRoom() {
|
||||||
|
enableJoinButton(false);
|
||||||
|
enableLeaveButton(true);
|
||||||
|
|
||||||
|
const meetingUrl = document.getElementById("meeting-url").value;
|
||||||
|
|
||||||
|
callObject.join({
|
||||||
|
url: meetingUrl,
|
||||||
|
startVideoOff: true,
|
||||||
|
startAudioOff: true,
|
||||||
|
subscribeToTracksAutomatically: false,
|
||||||
|
receiveSettings: {
|
||||||
|
base: { video: { layer: 0 } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leaveRoom() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
callObject.leave();
|
||||||
|
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
audioContainer.replaceChildren();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body onload="startDaily()">
|
||||||
|
<div class="ui centered page grid" style="margin-top: 30px">
|
||||||
|
<div class="ten wide column">
|
||||||
|
<div class="ui form" style="margin-top: 30px">
|
||||||
|
<div class="field">
|
||||||
|
<label>Meeting URL</label>
|
||||||
|
<input id="meeting-url" value="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui centered aligned header" style="margin-top: 30px">
|
||||||
|
<button id="join-button" class="ui primary button" onclick="joinRoom()">
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
<button id="leave-button" class="ui button" onclick="leaveRoom()">
|
||||||
|
Leave
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="tile" class="ui container" style="margin-top: 30px">
|
||||||
|
<div id="tile" class="ui center aligned grid">
|
||||||
|
<div id="audio-container"></div><br/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
examples/daily-custom-tracks/office-ambience-mono-16000.mp3
Normal file
2
examples/daily-custom-tracks/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pydub
|
||||||
|
pipecat-ai[daily]
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
FROM python:3.10-bullseye
|
FROM python:3.10-bullseye
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
|
RUN mkdir /app/assets
|
||||||
|
RUN mkdir /app/utils
|
||||||
COPY *.py /app/
|
COPY *.py /app/
|
||||||
COPY requirements.txt /app/
|
COPY requirements.txt /app/
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN pip3 install -r requirements.txt
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
39
examples/daily-multi-translation/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Daily Multi Translation
|
||||||
|
|
||||||
|
This example shows how to use Daily to stream multiple simultaneous translations using a single transport. Daily provides custom tracks and in this example we will simultaneously translate incoming audio in English to Spanish, French and German, each of them being sent to a custom track.
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
cp env.example .env # and add your credentials
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, visit `http://localhost:7860/` in your browser. This will open a Daily Prebuilt room where you will speak in English (make sure you are not muted).
|
||||||
|
|
||||||
|
## Open client
|
||||||
|
|
||||||
|
Next, you need to open the client that will listen to the translations.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the client is opened, copy the URL of the Daily room created above and join it. You should be able to select which translation you want to hear.
|
||||||
|
|
||||||
|
## Build and test the Docker image
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t daily-multi-translation .
|
||||||
|
docker run --env-file .env -p 7860:7860 daily-multi-translation
|
||||||
|
```
|
||||||
163
examples/daily-multi-translation/bot.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
from runner import configure
|
||||||
|
|
||||||
|
from pipecat.audio.mixers.soundfile_mixer import SoundfileMixer
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.observers.loggers.transcription_log_observer import TranscriptionLogObserver
|
||||||
|
from pipecat.pipeline.parallel_pipeline import ParallelPipeline
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||||
|
from pipecat.services.deepgram.stt import DeepgramSTTService
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
BACKGROUND_SOUND_FILE = "office-ambience-mono-16000.mp3"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
(room_url, token) = await configure(session)
|
||||||
|
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
token,
|
||||||
|
"Multi translation bot",
|
||||||
|
DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
audio_out_mixer={
|
||||||
|
"spanish": SoundfileMixer(
|
||||||
|
sound_files={"office": BACKGROUND_SOUND_FILE}, default_sound="office"
|
||||||
|
),
|
||||||
|
"french": SoundfileMixer(
|
||||||
|
sound_files={"office": BACKGROUND_SOUND_FILE}, default_sound="office"
|
||||||
|
),
|
||||||
|
"german": SoundfileMixer(
|
||||||
|
sound_files={"office": BACKGROUND_SOUND_FILE}, default_sound="office"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
audio_out_destinations=["spanish", "french", "german"],
|
||||||
|
microphone_out_enabled=False, # Disable since we just use custom tracks
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
||||||
|
|
||||||
|
tts_spanish = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="cefcb124-080b-4655-b31f-932f3ee743de",
|
||||||
|
transport_destination="spanish",
|
||||||
|
)
|
||||||
|
tts_french = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="8832a0b5-47b2-4751-bb22-6a8e2149303d",
|
||||||
|
transport_destination="french",
|
||||||
|
)
|
||||||
|
tts_german = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="38aabb6a-f52b-4fb0-a3d1-988518f4dc06",
|
||||||
|
transport_destination="german",
|
||||||
|
)
|
||||||
|
|
||||||
|
messages_spanish = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You will be provided with a sentence in English, and your task is to only translate it into Spanish.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
messages_french = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You will be provided with a sentence in English, and your task is to only translate it into French.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
messages_german = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You will be provided with a sentence in English, and your task is to only translate it into German.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
llm_spanish = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
llm_french = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
llm_german = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
context_spanish = OpenAILLMContext(messages_spanish)
|
||||||
|
context_aggregator_spanish = llm_spanish.create_context_aggregator(context_spanish)
|
||||||
|
|
||||||
|
context_french = OpenAILLMContext(messages_french)
|
||||||
|
context_aggregator_french = llm_french.create_context_aggregator(context_french)
|
||||||
|
|
||||||
|
context_german = OpenAILLMContext(messages_german)
|
||||||
|
context_aggregator_german = llm_german.create_context_aggregator(context_german)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(), # Transport user input
|
||||||
|
stt,
|
||||||
|
ParallelPipeline(
|
||||||
|
# Spanish pipeline.
|
||||||
|
[
|
||||||
|
context_aggregator_spanish.user(),
|
||||||
|
llm_spanish,
|
||||||
|
tts_spanish,
|
||||||
|
context_aggregator_spanish.assistant(),
|
||||||
|
],
|
||||||
|
# French pipeline.
|
||||||
|
[
|
||||||
|
context_aggregator_french.user(),
|
||||||
|
llm_french,
|
||||||
|
tts_french,
|
||||||
|
context_aggregator_french.assistant(),
|
||||||
|
],
|
||||||
|
# German pipeline.
|
||||||
|
[
|
||||||
|
context_aggregator_german.user(),
|
||||||
|
llm_german,
|
||||||
|
tts_german,
|
||||||
|
context_aggregator_german.assistant(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
transport.output(), # Transport bot output
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
audio_in_sample_rate=16000,
|
||||||
|
audio_out_sample_rate=16000,
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
observers=[TranscriptionLogObserver()],
|
||||||
|
)
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
DAILY_SAMPLE_ROOM_URL=https://yourdomain.daily.co/yourroom # (for joining the bot to the same room repeatedly for local dev)
|
DAILY_SAMPLE_ROOM_URL=https://yourdomain.daily.co/yourroom # (for joining the bot to the same room repeatedly for local dev)
|
||||||
DAILY_API_KEY=7df...
|
DAILY_API_KEY=7df...
|
||||||
OPENAI_API_KEY=sk-PL...
|
OPENAI_API_KEY=sk-PL...
|
||||||
ELEVENLABS_API_KEY=aeb...
|
DEEPGRAM_API_KEY=efb...
|
||||||
CANONICAL_API_KEY=can...
|
CARTESIA_API_KEY=aeb...
|
||||||
CANONICAL_API_URL=
|
|
||||||
202
examples/daily-multi-translation/index.html
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>daily multi translation</title>
|
||||||
|
</head>
|
||||||
|
<script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
|
||||||
|
<script
|
||||||
|
src="https://code.jquery.com/jquery-3.1.1.min.js"
|
||||||
|
integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.js"></script>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.css"
|
||||||
|
/>
|
||||||
|
<script>
|
||||||
|
function enableButton(buttonId, enable) {
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
button.disabled = !enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableJoinButton(enable) {
|
||||||
|
enableButton("join-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableLeaveButton(enable) {
|
||||||
|
enableButton("leave-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyPlayers(query) {
|
||||||
|
const items = document.querySelectorAll(query);
|
||||||
|
if (items) {
|
||||||
|
for (const item of items) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyParticipantPlayers(participantId) {
|
||||||
|
destroyPlayers(`video[data-participant-id="${participantId}"]`);
|
||||||
|
destroyPlayers(`audio[data-participant-id="${participantId}"]`);
|
||||||
|
destroyPlayers(`button[data-participant-id="${participantId}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startPlayer(player, track) {
|
||||||
|
player.muted = false;
|
||||||
|
player.autoplay = true;
|
||||||
|
if (track != null) {
|
||||||
|
player.srcObject = new MediaStream([track]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildVideoPlayer(track, participantId) {
|
||||||
|
const videoContainer = document.getElementById("video-container");
|
||||||
|
const player = document.createElement("video");
|
||||||
|
player.dataset.participantId = participantId;
|
||||||
|
|
||||||
|
videoContainer.appendChild(player);
|
||||||
|
|
||||||
|
await startPlayer(player, track);
|
||||||
|
await player.play();
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAudioPlayer(track, participantId) {
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
const player = document.createElement("audio");
|
||||||
|
player.dataset.participantId = participantId;
|
||||||
|
|
||||||
|
// Create a new button for controlling audio
|
||||||
|
const audioControlButton = document.createElement("button");
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
audioControlButton.innerText = track._mediaTag == "cam-audio" ? "english" : track._mediaTag;
|
||||||
|
audioControlButton.dataset.participantId = participantId;
|
||||||
|
audioControlButton.onclick = () => {
|
||||||
|
if (player.paused) {
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
audioControlButton.className = "ui primary red button"
|
||||||
|
} else {
|
||||||
|
player.pause();
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audioContainer.appendChild(player);
|
||||||
|
audioContainer.appendChild(audioControlButton);
|
||||||
|
|
||||||
|
await startPlayer(player, track);
|
||||||
|
player.pause()
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeToTracks(participantId) {
|
||||||
|
console.log(`subscribing to track`);
|
||||||
|
|
||||||
|
if (participantId === "local") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callObject.updateParticipant(participantId, {
|
||||||
|
setSubscribedTracks: {
|
||||||
|
audio: true,
|
||||||
|
video: true,
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDaily() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
window.callObject = window.DailyIframe.createCallObject({});
|
||||||
|
|
||||||
|
callObject.on("participant-joined", (e) => {
|
||||||
|
if (!e.participant.local) {
|
||||||
|
console.log("participant-joined", e.participant);
|
||||||
|
subscribeToTracks(e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("participant-left", (e) => {
|
||||||
|
console.log("participant-left", e.participant.session_id);
|
||||||
|
destroyParticipantPlayers(e.participant.session_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("track-started", async (e) => {
|
||||||
|
console.log("track-started", e.track);
|
||||||
|
if (e.track.kind === "video") {
|
||||||
|
await buildVideoPlayer(e.track, e.participant.session_id);
|
||||||
|
} else if (e.track.kind === "audio") {
|
||||||
|
await buildAudioPlayer(e.track, e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinRoom() {
|
||||||
|
enableJoinButton(false);
|
||||||
|
enableLeaveButton(true);
|
||||||
|
|
||||||
|
const meetingUrl = document.getElementById("meeting-url").value;
|
||||||
|
|
||||||
|
callObject.join({
|
||||||
|
url: meetingUrl,
|
||||||
|
startVideoOff: true,
|
||||||
|
startAudioOff: true,
|
||||||
|
subscribeToTracksAutomatically: false,
|
||||||
|
receiveSettings: {
|
||||||
|
base: { video: { layer: 0 } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leaveRoom() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
callObject.leave();
|
||||||
|
|
||||||
|
const videoContainer = document.getElementById("video-container");
|
||||||
|
videoContainer.replaceChildren();
|
||||||
|
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
audioContainer.replaceChildren();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body onload="startDaily()">
|
||||||
|
<div class="ui centered page grid" style="margin-top: 30px">
|
||||||
|
<div class="ten wide column">
|
||||||
|
<div class="ui form" style="margin-top: 30px">
|
||||||
|
<div class="field">
|
||||||
|
<label>Meeting URL</label>
|
||||||
|
<input id="meeting-url" value="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui centered aligned header" style="margin-top: 30px">
|
||||||
|
<button id="join-button" class="ui primary button" onclick="joinRoom()">
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
<button id="leave-button" class="ui button" onclick="leaveRoom()">
|
||||||
|
Leave
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="tile" class="ui container" style="margin-top: 30px">
|
||||||
|
<div id="tile" class="ui center aligned grid">
|
||||||
|
<div id="audio-container"></div><br/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tile" class="ui container" style="margin-top: 30px">
|
||||||
|
<div id="tile" class="ui center aligned grid">
|
||||||
|
<div id="video-container" class="ui segment"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
examples/daily-multi-translation/office-ambience-mono-16000.mp3
Normal file
5
examples/daily-multi-translation/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
aiofiles
|
||||||
|
python-dotenv
|
||||||
|
fastapi[all]
|
||||||
|
uvicorn
|
||||||
|
pipecat-ai[daily,deepgram,openai,silero,cartesia,soundfile]
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
@@ -14,15 +13,7 @@ from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper
|
|||||||
|
|
||||||
|
|
||||||
async def configure(aiohttp_session: aiohttp.ClientSession):
|
async def configure(aiohttp_session: aiohttp.ClientSession):
|
||||||
(url, token, _) = await configure_with_args(aiohttp_session)
|
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||||
return (url, token)
|
|
||||||
|
|
||||||
|
|
||||||
async def configure_with_args(
|
|
||||||
aiohttp_session: aiohttp.ClientSession, parser: Optional[argparse.ArgumentParser] = None
|
|
||||||
):
|
|
||||||
if not parser:
|
|
||||||
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-u", "--url", type=str, required=False, help="URL of the Daily room to join"
|
"-u", "--url", type=str, required=False, help="URL of the Daily room to join"
|
||||||
)
|
)
|
||||||
@@ -61,4 +52,4 @@ async def configure_with_args(
|
|||||||
|
|
||||||
token = await daily_rest_helper.get_token(url, expiry_time)
|
token = await daily_rest_helper.get_token(url, expiry_time)
|
||||||
|
|
||||||
return (url, token, args)
|
return (url, token)
|
||||||
@@ -75,7 +75,13 @@ async def main(room_url: str, token: str):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True))
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@transport.event_handler("on_first_participant_joined")
|
@transport.event_handler("on_first_participant_joined")
|
||||||
async def on_first_participant_joined(transport, participant):
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
|||||||
3
examples/deployment/modal-example/.gitignore
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
# Modal clone
|
||||||
|
modal-examples
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
@@ -1,37 +1,91 @@
|
|||||||
# Deploying Pipecat to Modal.com
|
# Deploying Pipecat to Modal.com
|
||||||
|
|
||||||
Barebones deployment example for [modal.com](https://www.modal.com)
|
Deployment example for [modal.com](https://www.modal.com). This example demonstrates how to deploy a FastAPI webapp to Modal with an RTVI compatible `/connect` endpoint that launches a Pipecat pipeline in a separate Modal container and returns a room/token for the client to join. This example also supports providing a parameter to the `/connect` endpoint for specifying which Pipecat pipeline to launch; openai, gemini, or vllm. The vllm pipeline points to a self-hosted OpenAI compatible LLM, using a llama model (neuralmagic/Meta-Llama-3.1-8B-Instruct-quantized.w4a16), deployed to Modal.
|
||||||
|
|
||||||
1. Install dependencies
|

|
||||||
|
|
||||||
```bash
|
# Running this Example
|
||||||
python -m venv venv
|
|
||||||
source venv/bin/active # or OS equivalent
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Setup .env
|
## Install the Modal CLI
|
||||||
|
|
||||||
```bash
|
Setup a Modal account and install it on your machine if you have not already, following their easy 3 steps in their [Getting Started Guide](https://modal.com/docs/guide#getting-started)
|
||||||
cp env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, you can configure your Modal app to use [secrets](https://modal.com/docs/guide/secrets)
|
## Deploy a self-serve LLM
|
||||||
|
|
||||||
3. Test the app locally
|
1. Deploy Modal's OpenAI-compatible LLM service:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
modal serve app.py
|
git clone https://github.com/modal-labs/modal-examples
|
||||||
```
|
cd modal-examples
|
||||||
|
modal deploy 06_gpu_and_ml/llm-serving/vllm_inference.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to Modal's guide and example for [Deploying an OpenAI-compatible LLM service with vLLM](https://modal.com/docs/examples/vllm_inference) for more details.
|
||||||
|
|
||||||
|
2. Take note of the endpoint URL from the previous step, which will look like:
|
||||||
|
```
|
||||||
|
https://{your-workspace}--example-vllm-openai-compatible-serve.modal.run
|
||||||
|
```
|
||||||
|
You'll need this for the `bot_vllm.py` file in the next section.
|
||||||
|
|
||||||
|
**Note:** The default Modal LLM example uses Llama-3.1 and will shut down after 15 minutes of inactivity. Cold starts take 5-10 minutes. To prepare the service, we recommend visiting the `/docs` endpoint (`https://<Modal workspace>--example-vllm-openai-compatible-serve.modal.run/docs`) for your deployed LLM and wait for it to fully load before connecting your client.
|
||||||
|
|
||||||
|
## Deploy FastAPI App and Pipecat pipeline to Modal
|
||||||
|
|
||||||
|
1. Setup environment variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
cp env.example .env
|
||||||
|
# Modify .env to provide your service API Keys
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can configure your Modal app to use [secrets](https://modal.com/docs/guide/secrets)
|
||||||
|
|
||||||
|
2. Update the `modal_url` in `server/src/bot_vllm.py` to point to the url produced from the self-serve llm deploy, mentioned above.
|
||||||
|
|
||||||
|
3. From within the `server` directory, test the app locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
modal serve app.py
|
||||||
|
```
|
||||||
|
|
||||||
4. Deploy to production
|
4. Deploy to production
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
modal deploy app.py
|
modal deploy app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration options
|
5. Note the endpoint URL produced from this deployment. It will look like:
|
||||||
|
|
||||||
This app sets some sensible defaults for reducing cold starts, such as `minkeep_warm=1`, which will keep at least 1 warm instance ready for your bot function.
|
```bash
|
||||||
|
https://{your-workspace}--pipecat-modal-fastapi-app.modal.run
|
||||||
|
```
|
||||||
|
|
||||||
It has been configured to only allow a concurrency of 1 (`max_inputs=1`) as each user will require their own running function.
|
You'll need this URL for the client's `app.js` configuration mentioned in its README.
|
||||||
|
|
||||||
|
## Launch your bots on Modal
|
||||||
|
|
||||||
|
### Option 1: Direct Link
|
||||||
|
|
||||||
|
Simply click on the url displayed after running the server or deploy step to launch an agent and be redirected to a Daily room to talk with the launched bot. This will use the OpenAI pipeline.
|
||||||
|
|
||||||
|
### Option 2: Connect via an RTVI Client
|
||||||
|
|
||||||
|
Follow the instructions provided in the [client folder's README](client/javascript/README.md) for building and running a custom client that connects to your Modal endpoint. The provided client provides a dropdown for choosing which bot pipeline to run.
|
||||||
|
|
||||||
|
# Navigating your llm, server, and Pipecat logs
|
||||||
|
|
||||||
|
In your [Modal dashboard](https://modal.com/apps), you should have two Apps listed under Live Apps:
|
||||||
|
|
||||||
|
1. `example-vllm-openai-compatible`: This App contains the containers and logs used to run your self-hosted LLM. There will be just one App Function listed: `serve`. Click on this function to view logs for your LLM.
|
||||||
|
2. `pipecat-modal`: This App contains the containers and logs used to run your `connect` endpoints and Pipecat pipelines. It will list two App Functions:
|
||||||
|
1. `fastapi_app`: This function is running the endpoints that your client will interact with and initiate starting a new pipeline (`/`, `/connect`, `/status`). Click on this function to see logs for each endpoint hit.
|
||||||
|
2. `bot_runner`: This function handles launching and running a bot pipeline. Click on this function to get a list of all pipeline runs and access each run's logs.
|
||||||
|
|
||||||
|
# Modal + Pipecat Tips
|
||||||
|
|
||||||
|
- In most other Pipecat examples, we use `Popen` to launch the pipeline process from the `/connect` endpoint. In this example, we use a Modal function instead. This allows us to run the pipelines using a separately defined Modal image as well as run each pipeline in an isolated container.
|
||||||
|
- For the FastAPI and most common Pipecat Pipeline containers, a default `debian_slim` CPU-only should be all that's required to run. GPU containers are needed for self-hosted services.
|
||||||
|
- To minimize cold starts of the pipeline and reduce latency for users, set `min_containers=1` on the Modal Function that launches the pipeline to ensure at least one warm instance of your function is always available.
|
||||||
|
- For next steps on running a self-hosted llm and reducing latency, check out all of [Modal's LLM examples](https://modal.com/docs/examples/vllm_inference).
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024–2025, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import modal
|
|
||||||
from bot import _voice_bot_process
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
MAX_SESSION_TIME = 15 * 60 # 15 minutes
|
|
||||||
|
|
||||||
app = modal.App("pipecat-modal")
|
|
||||||
|
|
||||||
|
|
||||||
image = modal.Image.debian_slim(python_version="3.12").pip_install_from_requirements(
|
|
||||||
"requirements.txt"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.function(
|
|
||||||
image=image,
|
|
||||||
cpu=1.0,
|
|
||||||
secrets=[modal.Secret.from_dotenv()],
|
|
||||||
keep_warm=1,
|
|
||||||
enable_memory_snapshot=True,
|
|
||||||
max_inputs=1, # Do not reuse instances across requests
|
|
||||||
retries=0,
|
|
||||||
)
|
|
||||||
def launch_bot_process(room_url: str, token: str):
|
|
||||||
_voice_bot_process(room_url, token)
|
|
||||||
|
|
||||||
|
|
||||||
@app.function(
|
|
||||||
image=image,
|
|
||||||
secrets=[modal.Secret.from_dotenv()],
|
|
||||||
)
|
|
||||||
@modal.web_endpoint(method="POST")
|
|
||||||
async def start():
|
|
||||||
from pipecat.transports.services.helpers.daily_rest import (
|
|
||||||
DailyRESTHelper,
|
|
||||||
DailyRoomParams,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Request received")
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
daily_rest_helper = DailyRESTHelper(
|
|
||||||
daily_api_key=os.getenv("DAILY_API_KEY", ""),
|
|
||||||
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
|
||||||
aiohttp_session=session,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create new Daily room
|
|
||||||
room = await daily_rest_helper.create_room(DailyRoomParams())
|
|
||||||
if not room.url:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail="Unable to create room",
|
|
||||||
)
|
|
||||||
logger.info(f"Created room: {room.url}")
|
|
||||||
|
|
||||||
# Create bot token for room
|
|
||||||
token = await daily_rest_helper.get_token(room.url, MAX_SESSION_TIME)
|
|
||||||
if not token:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to get token for room: {room.url}")
|
|
||||||
|
|
||||||
logger.info(f"Bot token created: {token}")
|
|
||||||
|
|
||||||
# Spawn a new bot process
|
|
||||||
launch_bot_process.spawn(room_url=room.url, token=token)
|
|
||||||
|
|
||||||
# Return room URL to the user to join
|
|
||||||
# Note: in production, you would want to return a token to the user
|
|
||||||
return JSONResponse(content={"room_url": room.url, token: token})
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2024–2025, Daily
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
|
||||||
#
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
|
||||||
from pipecat.pipeline.pipeline import Pipeline
|
|
||||||
from pipecat.pipeline.runner import PipelineRunner
|
|
||||||
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
||||||
from pipecat.services.cartesia.tts import CartesiaTTSService
|
|
||||||
from pipecat.services.openai.llm import OpenAILLMService
|
|
||||||
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
|
||||||
|
|
||||||
load_dotenv(override=True)
|
|
||||||
|
|
||||||
logger.remove(0)
|
|
||||||
logger.add(sys.stderr, level="DEBUG")
|
|
||||||
|
|
||||||
|
|
||||||
async def main(room_url: str, token: str):
|
|
||||||
transport = DailyTransport(
|
|
||||||
room_url,
|
|
||||||
token,
|
|
||||||
"bot",
|
|
||||||
DailyParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
transcription_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
tts = CartesiaTTSService(
|
|
||||||
api_key=os.getenv("CARTESIA_API_KEY", ""), voice_id="71a7ad14-091c-4e8e-a314-022ece01c121"
|
|
||||||
)
|
|
||||||
|
|
||||||
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
|
||||||
|
|
||||||
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 so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
context = OpenAILLMContext(messages)
|
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
|
||||||
|
|
||||||
pipeline = Pipeline(
|
|
||||||
[
|
|
||||||
transport.input(),
|
|
||||||
context_aggregator.user(),
|
|
||||||
llm,
|
|
||||||
tts,
|
|
||||||
transport.output(),
|
|
||||||
context_aggregator.assistant(),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
task = PipelineTask(
|
|
||||||
pipeline,
|
|
||||||
params=PipelineParams(
|
|
||||||
allow_interruptions=True,
|
|
||||||
enable_metrics=True,
|
|
||||||
enable_usage_metrics=True,
|
|
||||||
report_only_initial_ttfb=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@transport.event_handler("on_first_participant_joined")
|
|
||||||
async def on_first_participant_joined(transport, participant):
|
|
||||||
await transport.capture_participant_transcription(participant["id"])
|
|
||||||
messages.append({"role": "system", "content": "Please introduce yourself to the user."})
|
|
||||||
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
|
||||||
|
|
||||||
@transport.event_handler("on_participant_left")
|
|
||||||
async def on_participant_left(transport, participant, reason):
|
|
||||||
await task.cancel()
|
|
||||||
|
|
||||||
runner = PipelineRunner()
|
|
||||||
|
|
||||||
await runner.run(task)
|
|
||||||
|
|
||||||
|
|
||||||
def _voice_bot_process(room_url: str, token: str):
|
|
||||||
asyncio.run(main(room_url, token))
|
|
||||||
1
examples/deployment/modal-example/client/javascript/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# JavaScript Implementation
|
||||||
|
|
||||||
|
Basic implementation using the [Pipecat JavaScript SDK](https://docs.pipecat.ai/client/js/introduction).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Deploy the Modal server. See the main [README](../../README).
|
||||||
|
|
||||||
|
2. Navigate to the `client/javascript` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd client/javascript
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Modify the baseUrl in src/app.js to point to your deployed Modal endpoint
|
||||||
|
|
||||||
|
4. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Run the client app:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Visit http://localhost:5173 in your browser.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AI Chatbot</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="status-bar">
|
||||||
|
<div class="status">
|
||||||
|
Status: <span id="connection-status">Disconnected</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<select id="bot-selector">
|
||||||
|
<option value="openai">OpenAI</option>
|
||||||
|
<option value="gemini">Gemini</option>
|
||||||
|
<option value="vllm">Llama</option>
|
||||||
|
</select>
|
||||||
|
<button id="connect-btn">Connect</button>
|
||||||
|
<button id="disconnect-btn" disabled>Disconnect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="bot-container">
|
||||||
|
<div id="bot-video-container"></div>
|
||||||
|
<audio id="bot-audio" autoplay></audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="device-bar">
|
||||||
|
<div class="device-controls">
|
||||||
|
<select id="device-selector"></select>
|
||||||
|
<button id="mic-toggle-btn">Mute Mic</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="debug-panel">
|
||||||
|
<h3>Debug Info</h3>
|
||||||
|
<div id="debug-log"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/src/app.js"></script>
|
||||||
|
<link rel="stylesheet" href="/src/style.css" />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1282
examples/deployment/modal-example/client/javascript/package-lock.json
generated
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.3.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@pipecat-ai/client-js": "^1.0.0",
|
||||||
|
"@pipecat-ai/daily-transport": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
376
examples/deployment/modal-example/client/javascript/src/app.js
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024–2025, Daily
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipecat Client Implementation
|
||||||
|
*
|
||||||
|
* This client connects to an RTVI-compatible bot server using WebRTC (via Daily).
|
||||||
|
* It handles audio/video streaming and manages the connection lifecycle.
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
* - A running RTVI bot server (defaults to http://localhost:7860)
|
||||||
|
* - The server must implement the /connect endpoint that returns Daily.co room credentials
|
||||||
|
* - Browser with WebRTC support
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PipecatClient, RTVIEvent } from '@pipecat-ai/client-js';
|
||||||
|
import { DailyTransport } from '@pipecat-ai/daily-transport';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatbotClient handles the connection and media management for a real-time
|
||||||
|
* voice and video interaction with an AI bot.
|
||||||
|
*/
|
||||||
|
class ChatbotClient {
|
||||||
|
constructor() {
|
||||||
|
// Initialize client state
|
||||||
|
this.pcClient = null;
|
||||||
|
this.setupDOMElements();
|
||||||
|
this.initializeClientAndTransport();
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up references to DOM elements and create necessary media elements
|
||||||
|
*/
|
||||||
|
setupDOMElements() {
|
||||||
|
// Get references to UI control elements
|
||||||
|
this.connectBtn = document.getElementById('connect-btn');
|
||||||
|
this.disconnectBtn = document.getElementById('disconnect-btn');
|
||||||
|
this.statusSpan = document.getElementById('connection-status');
|
||||||
|
this.debugLog = document.getElementById('debug-log');
|
||||||
|
this.botVideoContainer = document.getElementById('bot-video-container');
|
||||||
|
this.deviceSelector = document.getElementById('device-selector');
|
||||||
|
|
||||||
|
// Create an audio element for bot's voice output
|
||||||
|
this.botAudio = document.createElement('audio');
|
||||||
|
this.botAudio.autoplay = true;
|
||||||
|
this.botAudio.playsInline = true;
|
||||||
|
document.body.appendChild(this.botAudio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up event listeners for connect/disconnect buttons
|
||||||
|
*/
|
||||||
|
setupEventListeners() {
|
||||||
|
this.connectBtn.addEventListener('click', () => this.connect());
|
||||||
|
this.disconnectBtn.addEventListener('click', () => this.disconnect());
|
||||||
|
|
||||||
|
// Populate device selector
|
||||||
|
this.pcClient.getAllMics().then((mics) => {
|
||||||
|
console.log('Available mics:', mics);
|
||||||
|
mics.forEach((device) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = device.deviceId;
|
||||||
|
option.textContent = device.label || `Microphone ${device.deviceId}`;
|
||||||
|
this.deviceSelector.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.deviceSelector.addEventListener('change', (event) => {
|
||||||
|
const selectedDeviceId = event.target.value;
|
||||||
|
console.log('Selected device ID:', selectedDeviceId);
|
||||||
|
this.pcClient.updateMic(selectedDeviceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle mic mute/unmute toggle
|
||||||
|
const micToggleBtn = document.getElementById('mic-toggle-btn');
|
||||||
|
|
||||||
|
micToggleBtn.addEventListener('click', () => {
|
||||||
|
let micEnabled = this.pcClient.isMicEnabled;
|
||||||
|
micToggleBtn.textContent = micEnabled ? 'Unmute Mic' : 'Mute Mic';
|
||||||
|
this.pcClient.enableMic(!micEnabled);
|
||||||
|
// Add logic to mute/unmute the mic
|
||||||
|
if (micEnabled) {
|
||||||
|
console.log('Mic muted');
|
||||||
|
// Add code to mute the mic
|
||||||
|
} else {
|
||||||
|
console.log('Mic unmuted');
|
||||||
|
// Add code to unmute the mic
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the Pipecat client and Daily transport
|
||||||
|
*/
|
||||||
|
async initializeClientAndTransport() {
|
||||||
|
// Initialize the Pipecat client with a DailyTransport and our configuration
|
||||||
|
this.pcClient = new PipecatClient({
|
||||||
|
transport: new DailyTransport(),
|
||||||
|
enableMic: true, // Enable microphone for user input
|
||||||
|
enableCam: false,
|
||||||
|
callbacks: {
|
||||||
|
// Handle connection state changes
|
||||||
|
onConnected: () => {
|
||||||
|
this.updateStatus('Connected');
|
||||||
|
this.connectBtn.disabled = true;
|
||||||
|
this.disconnectBtn.disabled = false;
|
||||||
|
this.log('Client connected');
|
||||||
|
},
|
||||||
|
onDisconnected: () => {
|
||||||
|
this.updateStatus('Disconnected');
|
||||||
|
this.connectBtn.disabled = false;
|
||||||
|
this.disconnectBtn.disabled = true;
|
||||||
|
this.log('Client disconnected');
|
||||||
|
},
|
||||||
|
// Handle transport state changes
|
||||||
|
onTransportStateChanged: (state) => {
|
||||||
|
this.updateStatus(`Transport: ${state}`);
|
||||||
|
this.log(`Transport state changed: ${state}`);
|
||||||
|
if (state === 'connecting') {
|
||||||
|
window.startTime = Date.now();
|
||||||
|
}
|
||||||
|
if (state === 'ready') {
|
||||||
|
this.setupMediaTracks();
|
||||||
|
console.warn('TIME TO BOT READY:', Date.now() - window.startTime);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Handle bot connection events
|
||||||
|
onBotConnected: (participant) => {
|
||||||
|
this.log(`Bot connected: ${JSON.stringify(participant)}`);
|
||||||
|
},
|
||||||
|
onBotDisconnected: (participant) => {
|
||||||
|
this.log(`Bot disconnected: ${JSON.stringify(participant)}`);
|
||||||
|
},
|
||||||
|
onBotReady: (data) => {
|
||||||
|
this.log(`Bot ready: ${JSON.stringify(data)}`);
|
||||||
|
this.setupMediaTracks();
|
||||||
|
},
|
||||||
|
// Transcript events
|
||||||
|
onUserTranscript: (data) => {
|
||||||
|
// Only log final transcripts
|
||||||
|
if (data.final) {
|
||||||
|
this.log(`User: ${data.text}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBotTranscript: (data) => {
|
||||||
|
this.log(`Bot: ${data.text}`);
|
||||||
|
},
|
||||||
|
// Error handling
|
||||||
|
onMessageError: (error) => {
|
||||||
|
console.log('Message error:', error);
|
||||||
|
},
|
||||||
|
onMicUpdated: (data) => {
|
||||||
|
console.log('Mic updated:', data);
|
||||||
|
this.deviceSelector.value = data.deviceId;
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.log('Error:', JSON.stringify(error));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up listeners for media track events
|
||||||
|
this.setupTrackListeners();
|
||||||
|
|
||||||
|
await this.pcClient.initDevices();
|
||||||
|
window.client = this.pcClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a timestamped message to the debug log
|
||||||
|
*/
|
||||||
|
log(message) {
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
entry.textContent = `${new Date().toISOString()} - ${message}`;
|
||||||
|
|
||||||
|
// Add styling based on message type
|
||||||
|
if (message.startsWith('User: ')) {
|
||||||
|
entry.style.color = '#2196F3'; // blue for user
|
||||||
|
} else if (message.startsWith('Bot: ')) {
|
||||||
|
entry.style.color = '#4CAF50'; // green for bot
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debugLog.appendChild(entry);
|
||||||
|
this.debugLog.scrollTop = this.debugLog.scrollHeight;
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the connection status display
|
||||||
|
*/
|
||||||
|
updateStatus(status) {
|
||||||
|
this.statusSpan.textContent = status;
|
||||||
|
this.log(`Status: ${status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for available media tracks and set them up if present
|
||||||
|
* This is called when the bot is ready or when the transport state changes to ready
|
||||||
|
*/
|
||||||
|
setupMediaTracks() {
|
||||||
|
if (!this.pcClient) return;
|
||||||
|
|
||||||
|
// Get current tracks from the client
|
||||||
|
const tracks = this.pcClient.tracks();
|
||||||
|
|
||||||
|
// Set up any available bot tracks
|
||||||
|
if (tracks.bot?.audio) {
|
||||||
|
this.setupAudioTrack(tracks.bot.audio);
|
||||||
|
}
|
||||||
|
if (tracks.bot?.video) {
|
||||||
|
this.setupVideoTrack(tracks.bot.video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up listeners for track events (start/stop)
|
||||||
|
* This handles new tracks being added during the session
|
||||||
|
*/
|
||||||
|
setupTrackListeners() {
|
||||||
|
if (!this.pcClient) return;
|
||||||
|
|
||||||
|
// Listen for new tracks starting
|
||||||
|
this.pcClient.on(RTVIEvent.TrackStarted, (track, participant) => {
|
||||||
|
// Only handle non-local (bot) tracks
|
||||||
|
if (!participant?.local) {
|
||||||
|
if (track.kind === 'audio') {
|
||||||
|
this.setupAudioTrack(track);
|
||||||
|
} else if (track.kind === 'video') {
|
||||||
|
this.setupVideoTrack(track);
|
||||||
|
}
|
||||||
|
this.log(
|
||||||
|
`Track started event: ${track.kind} from ${
|
||||||
|
participant?.name || 'unknown'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.log('Local mic unmuted');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for tracks stopping
|
||||||
|
this.pcClient.on(RTVIEvent.TrackStopped, (track, participant) => {
|
||||||
|
if (participant.local) {
|
||||||
|
this.log('Local mic muted');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.log(
|
||||||
|
`Track stopped event: ${track.kind} from ${
|
||||||
|
participant?.name || 'unknown'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up an audio track for playback
|
||||||
|
* Handles both initial setup and track updates
|
||||||
|
*/
|
||||||
|
setupAudioTrack(track) {
|
||||||
|
this.log('Setting up audio track');
|
||||||
|
// Check if we're already playing this track
|
||||||
|
if (this.botAudio.srcObject) {
|
||||||
|
const oldTrack = this.botAudio.srcObject.getAudioTracks()[0];
|
||||||
|
if (oldTrack?.id === track.id) return;
|
||||||
|
}
|
||||||
|
// Create a new MediaStream with the track and set it as the audio source
|
||||||
|
this.botAudio.srcObject = new MediaStream([track]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up a video track for display
|
||||||
|
* Handles both initial setup and track updates
|
||||||
|
*/
|
||||||
|
setupVideoTrack(track) {
|
||||||
|
this.log('Setting up video track');
|
||||||
|
const videoEl = document.createElement('video');
|
||||||
|
videoEl.autoplay = true;
|
||||||
|
videoEl.playsInline = true;
|
||||||
|
videoEl.muted = true;
|
||||||
|
videoEl.style.width = '100%';
|
||||||
|
videoEl.style.height = '100%';
|
||||||
|
videoEl.style.objectFit = 'cover';
|
||||||
|
|
||||||
|
// Check if we're already displaying this track
|
||||||
|
if (this.botVideoContainer.querySelector('video')?.srcObject) {
|
||||||
|
const oldTrack = this.botVideoContainer
|
||||||
|
.querySelector('video')
|
||||||
|
.srcObject.getVideoTracks()[0];
|
||||||
|
if (oldTrack?.id === track.id) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new MediaStream with the track and set it as the video source
|
||||||
|
videoEl.srcObject = new MediaStream([track]);
|
||||||
|
this.botVideoContainer.innerHTML = '';
|
||||||
|
this.botVideoContainer.appendChild(videoEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize and connect to the bot
|
||||||
|
* This sets up the Pipecat client, initializes devices, and establishes the connection
|
||||||
|
*/
|
||||||
|
async connect() {
|
||||||
|
try {
|
||||||
|
const botSelector = document.getElementById('bot-selector');
|
||||||
|
const selectedBot = botSelector.value;
|
||||||
|
|
||||||
|
// Initialize audio/video devices
|
||||||
|
this.log('Initializing devices...');
|
||||||
|
await this.pcClient.initDevices();
|
||||||
|
|
||||||
|
// Connect to the bot
|
||||||
|
this.log(`Connecting to bot: ${selectedBot}`);
|
||||||
|
await this.pcClient.connect({
|
||||||
|
// REPLACE WITH YOUR MODAL URL ENDPOINT
|
||||||
|
endpoint:
|
||||||
|
'https://<your-workspace>--pipecat-modal-fastapi-app.modal.run/connect',
|
||||||
|
requestData: {
|
||||||
|
bot_name: selectedBot,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log('Connection complete');
|
||||||
|
} catch (error) {
|
||||||
|
// Handle any errors during connection
|
||||||
|
console.error('Connection error:', error);
|
||||||
|
this.log(`Error connecting: ${JSON.stringify(error.message)}`);
|
||||||
|
this.log(`Error stack: ${error.stack}`);
|
||||||
|
this.updateStatus('Error');
|
||||||
|
|
||||||
|
// Clean up if there's an error
|
||||||
|
if (this.pcClient) {
|
||||||
|
try {
|
||||||
|
await this.pcClient.disconnect();
|
||||||
|
} catch (disconnectError) {
|
||||||
|
this.log(`Error during disconnect: ${disconnectError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the bot and clean up media resources
|
||||||
|
*/
|
||||||
|
async disconnect() {
|
||||||
|
if (this.pcClient) {
|
||||||
|
try {
|
||||||
|
// Disconnect the Pipecat client
|
||||||
|
await this.pcClient.disconnect();
|
||||||
|
|
||||||
|
// Clean up audio
|
||||||
|
if (this.botAudio.srcObject) {
|
||||||
|
this.botAudio.srcObject.getTracks().forEach((track) => track.stop());
|
||||||
|
this.botAudio.srcObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up video
|
||||||
|
if (this.botVideoContainer.querySelector('video')?.srcObject) {
|
||||||
|
const video = this.botVideoContainer.querySelector('video');
|
||||||
|
video.srcObject.getTracks().forEach((track) => track.stop());
|
||||||
|
video.srcObject = null;
|
||||||
|
}
|
||||||
|
this.botVideoContainer.innerHTML = '';
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`Error disconnecting: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the client when the page loads
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new ChatbotClient();
|
||||||
|
});
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar,
|
||||||
|
.device-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls,
|
||||||
|
.device-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px; /* Adds spacing between elements */
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-controls {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button,
|
||||||
|
.device-controls button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin-left: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bot-selector,
|
||||||
|
#device-selector {
|
||||||
|
padding: 8px 16px;
|
||||||
|
padding-right: 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #6c757d; /* Gray background */
|
||||||
|
color: white; /* White text */
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none; /* Removes default browser styling for dropdowns */
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); /* Custom arrow */
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 8px center; /* Position the arrow */
|
||||||
|
}
|
||||||
|
|
||||||
|
#bot-selector:focus,
|
||||||
|
#device-selector:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); /* Add a subtle focus effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
#connect-btn {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#disconnect-btn {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mic-toggle-btn {
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bot-video-container {
|
||||||
|
width: 640px;
|
||||||
|
height: 360px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px auto;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bot-video-container video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#debug-log {
|
||||||
|
height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
BIN
examples/deployment/modal-example/diagram.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
@@ -1,3 +0,0 @@
|
|||||||
DAILY_API_KEY=
|
|
||||||
OPENAI_API_KEY=
|
|
||||||
CARTESIA_API_KEY=
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
python-dotenv==1.0.1
|
|
||||||
modal==0.71.3
|
|
||||||
pipecat-ai[daily,silero,cartesia,openai]
|
|
||||||
fastapi==0.115.6
|
|
||||||
307
examples/deployment/modal-example/server/app.py
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
"""modal_example.
|
||||||
|
|
||||||
|
This module shows a simple example of how to deploy a bot using Modal and FastAPI.
|
||||||
|
|
||||||
|
It includes:
|
||||||
|
- FastAPI endpoints for starting agents and checking bot statuses.
|
||||||
|
- Dynamic loading of bot implementations.
|
||||||
|
- Use of a Daily transport for bot communication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Any, Dict, Literal
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import modal
|
||||||
|
from fastapi import APIRouter, FastAPI, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
# container specifications for the FastAPI web server
|
||||||
|
web_image = (
|
||||||
|
modal.Image.debian_slim(python_version="3.13")
|
||||||
|
.pip_install_from_requirements("requirements.txt")
|
||||||
|
.pip_install("pipecat-ai[daily]")
|
||||||
|
.add_local_dir("src", remote_path="/root/src")
|
||||||
|
)
|
||||||
|
|
||||||
|
# container specifications for the Pipecat pipeline
|
||||||
|
bot_image = (
|
||||||
|
modal.Image.debian_slim(python_version="3.13")
|
||||||
|
.apt_install("ffmpeg")
|
||||||
|
.pip_install_from_requirements("requirements.txt")
|
||||||
|
.pip_install("pipecat-ai[daily,elevenlabs,openai,silero,google]")
|
||||||
|
.add_local_dir("src", remote_path="/root/src")
|
||||||
|
)
|
||||||
|
|
||||||
|
app = modal.App("pipecat-modal", secrets=[modal.Secret.from_dotenv()])
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
bot_jobs = {}
|
||||||
|
daily_helpers = {}
|
||||||
|
|
||||||
|
# Names of all supported bot implementations
|
||||||
|
# These correspond to the bot files in the src directory
|
||||||
|
BotName = Literal["openai", "gemini", "vllm"]
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
"""Cleanup function to terminate all bot processes.
|
||||||
|
|
||||||
|
Called during server shutdown.
|
||||||
|
"""
|
||||||
|
for entry in bot_jobs.values():
|
||||||
|
func = modal.FunctionCall.from_id(entry[0])
|
||||||
|
if func:
|
||||||
|
func.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot_file(bot_name: BotName) -> str:
|
||||||
|
"""Retrieve the bot file name corresponding to the provided bot_name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_name (BotName): The name of the bot (e.g., 'openai', 'gemini', 'vllm').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The file name corresponding to the bot implementation.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the bot name is invalid or not supported.
|
||||||
|
"""
|
||||||
|
# bot_implementation = os.getenv("BOT_IMPLEMENTATION", "openai").lower().strip()
|
||||||
|
bot_implementation = bot_name.lower().strip()
|
||||||
|
if not bot_implementation:
|
||||||
|
bot_implementation = "openai"
|
||||||
|
if bot_implementation not in ["openai", "gemini", "vllm"]:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid BOT_IMPLEMENTATION: {bot_implementation}. Must be 'openai' or 'gemini' or 'vllm'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"bot_{bot_implementation}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_runner(path: str, bot_file: str) -> callable:
|
||||||
|
"""Dynamically import the run_bot function based on the bot name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The path to the bot files (e.g., 'src').
|
||||||
|
bot_file (str): The file name of the bot implementation (e.g., 'openai', 'gemini', 'vllm').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
function: The run_bot function from the specified bot module.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ImportError: If the specified bot module or run_bot function is not found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Dynamically construct the module name
|
||||||
|
module_name = f"{path}.{bot_file}"
|
||||||
|
# Import the module
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
# Get the run_bot function from the module
|
||||||
|
return getattr(module, "run_bot")
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
raise ImportError(f"Failed to import run_bot from {module_name}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_room_and_token() -> tuple[str, str]:
|
||||||
|
"""Create a Daily room and generate an authentication token.
|
||||||
|
|
||||||
|
This function checks for existing room URL and token in the environment variables.
|
||||||
|
If not found, it creates a new room using the Daily API and generates a token for it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[str, str]: A tuple containing the room URL and the authentication token.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If room creation or token generation fails.
|
||||||
|
"""
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import DailyRoomParams
|
||||||
|
|
||||||
|
room_url = os.getenv("DAILY_SAMPLE_ROOM_URL", None)
|
||||||
|
token = os.getenv("DAILY_SAMPLE_ROOM_TOKEN", None)
|
||||||
|
if not room_url:
|
||||||
|
room = await daily_helpers["rest"].create_room(DailyRoomParams())
|
||||||
|
if not room.url:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to create room")
|
||||||
|
room_url = room.url
|
||||||
|
|
||||||
|
token = await daily_helpers["rest"].get_token(room_url)
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get token for room: {room_url}")
|
||||||
|
|
||||||
|
return room_url, token
|
||||||
|
|
||||||
|
|
||||||
|
@app.function(image=bot_image, min_containers=1)
|
||||||
|
async def bot_runner(room_url, token, bot_name: BotName = "openai"):
|
||||||
|
"""Launch the provided bot process, providing the given room URL and token for the bot to join.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room_url (str): The URL of the Daily room where the bot and client will communicate.
|
||||||
|
token (str): The authentication token for the room.
|
||||||
|
bot_name (BotName): The name of the bot implementation to use. Defaults to "openai".
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If the bot pipeline fails to start.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
path = "src"
|
||||||
|
bot_file = get_bot_file(bot_name)
|
||||||
|
run_bot = get_runner(path, bot_file)
|
||||||
|
|
||||||
|
print(f"Starting bot process: {bot_file} -u {room_url} -t {token}")
|
||||||
|
await run_bot(room_url, token)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to start bot pipeline: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""FastAPI lifespan manager that handles startup and shutdown tasks.
|
||||||
|
|
||||||
|
- Creates aiohttp session
|
||||||
|
- Initializes Daily API helper
|
||||||
|
- Cleans up resources on shutdown
|
||||||
|
"""
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper
|
||||||
|
|
||||||
|
aiohttp_session = aiohttp.ClientSession()
|
||||||
|
daily_helpers["rest"] = DailyRESTHelper(
|
||||||
|
daily_api_key=os.getenv("DAILY_API_KEY", ""),
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=aiohttp_session,
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
await aiohttp_session.close()
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectData(BaseModel):
|
||||||
|
"""Data provided by client to specify the bot pipeline.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
bot_name (BotName): The name of the bot to connect to. Defaults to "openai".
|
||||||
|
"""
|
||||||
|
|
||||||
|
bot_name: BotName = "openai"
|
||||||
|
|
||||||
|
|
||||||
|
async def start(data: ConnectData):
|
||||||
|
"""Internal method to start a bot agent and return the room URL and token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (ConnectData): The data containing the bot name to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[str, str]: A tuple containing the room URL and token.
|
||||||
|
"""
|
||||||
|
room_url, token = await create_room_and_token()
|
||||||
|
launch_bot_func = modal.Function.from_name("pipecat-modal", "bot_runner")
|
||||||
|
function_id = launch_bot_func.spawn(room_url, token, data.bot_name)
|
||||||
|
bot_jobs[function_id] = (function_id, room_url)
|
||||||
|
|
||||||
|
return room_url, token
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def start_agent():
|
||||||
|
"""A user endpoint for launching a bot agent and redirecting to the created room URL.
|
||||||
|
|
||||||
|
This function retrieves the bot implementation from the environment,
|
||||||
|
starts the bot agent, and redirects the user to the room URL to
|
||||||
|
interact with the bot through a Daily Prebuilt Interface.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RedirectResponse: A response that redirects to the room URL.
|
||||||
|
"""
|
||||||
|
bot_name = os.getenv("BOT_IMPLEMENTATION", "openai").lower().strip()
|
||||||
|
print(f"Starting bot: {bot_name}")
|
||||||
|
room_url, token = await start(ConnectData(bot_name=bot_name))
|
||||||
|
|
||||||
|
return RedirectResponse(room_url)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/connect")
|
||||||
|
async def rtvi_connect(data: ConnectData) -> Dict[Any, Any]:
|
||||||
|
"""A user endpoint for launching a bot agent and retrieving the room/token credentials.
|
||||||
|
|
||||||
|
This function retrieves the bot implementation from the request, if provided,
|
||||||
|
starts the bot agent, and returns the room URL and token for the bot. This allows the
|
||||||
|
client to then connect to the bot using their own RTVI interface.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (ConnectData): Optional. The data containing the bot name to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[Any, Any]: A dictionary containing the room URL and token.
|
||||||
|
"""
|
||||||
|
print(f"Starting bot: {data.bot_name}")
|
||||||
|
if data is None or not data.bot_name:
|
||||||
|
data.bot_name = os.getenv("BOT_IMPLEMENTATION", "openai").lower().strip()
|
||||||
|
room_url, token = await start(data)
|
||||||
|
|
||||||
|
return {"room_url": room_url, "token": token}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/status/{fid}")
|
||||||
|
def get_status(fid: str):
|
||||||
|
"""Retrieve the status of a bot process by its function ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fid (str): The function ID of the bot process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSONResponse: A JSON response containing the bot's status and result code.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If the bot process with the given ID is not found.
|
||||||
|
"""
|
||||||
|
func = modal.FunctionCall.from_id(fid)
|
||||||
|
if not func:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Bot with process id: {fid} not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = func.get(timeout=0)
|
||||||
|
return JSONResponse({"bot_id": fid, "status": "finished", "code": result})
|
||||||
|
except modal.exception.OutputExpiredError:
|
||||||
|
return JSONResponse({"bot_id": fid, "status": "finished", "code": 404})
|
||||||
|
except TimeoutError:
|
||||||
|
return JSONResponse({"bot_id": fid, "status": "running", "code": 202})
|
||||||
|
|
||||||
|
|
||||||
|
@app.function(image=web_image, min_containers=1)
|
||||||
|
@modal.concurrent(max_inputs=1)
|
||||||
|
@modal.asgi_app()
|
||||||
|
def fastapi_app():
|
||||||
|
"""Create and configure the FastAPI application.
|
||||||
|
|
||||||
|
This function initializes the FastAPI app with middleware, routes, and lifespan management.
|
||||||
|
It is decorated to be used as a Modal ASGI app.
|
||||||
|
"""
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
# Initialize FastAPI app
|
||||||
|
web_app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
web_app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include the endpoints from this file
|
||||||
|
web_app.include_router(router)
|
||||||
|
|
||||||
|
return web_app
|
||||||
14
examples/deployment/modal-example/server/env.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
DAILY_API_KEY=
|
||||||
|
|
||||||
|
# determines which bot file to default to: 'openai', 'gemini', or 'vllm'
|
||||||
|
BOT_IMPLEMENTATION=openai
|
||||||
|
|
||||||
|
# needed for the openai bot pipeline
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
ELEVENLABS_API_KEY=
|
||||||
|
|
||||||
|
# needed for the gemini live bot pipeline
|
||||||
|
GOOGLE_API_KEY=
|
||||||
|
|
||||||
|
# needed if you modified the API Key for your self-hosted LLM
|
||||||
|
VLLM_API_KEY=
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
python-dotenv==1.0.1
|
||||||
|
modal==1.0.5
|
||||||
|
fastapi[all]
|
||||||
BIN
examples/deployment/modal-example/server/src/assets/robot01.png
Normal file
|
After Width: | Height: | Size: 759 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot010.png
Normal file
|
After Width: | Height: | Size: 884 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot011.png
Normal file
|
After Width: | Height: | Size: 876 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot012.png
Normal file
|
After Width: | Height: | Size: 881 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot013.png
Normal file
|
After Width: | Height: | Size: 866 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot014.png
Normal file
|
After Width: | Height: | Size: 874 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot015.png
Normal file
|
After Width: | Height: | Size: 882 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot016.png
Normal file
|
After Width: | Height: | Size: 885 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot017.png
Normal file
|
After Width: | Height: | Size: 888 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot018.png
Normal file
|
After Width: | Height: | Size: 890 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot019.png
Normal file
|
After Width: | Height: | Size: 898 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot02.png
Normal file
|
After Width: | Height: | Size: 836 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot020.png
Normal file
|
After Width: | Height: | Size: 903 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot021.png
Normal file
|
After Width: | Height: | Size: 908 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot022.png
Normal file
|
After Width: | Height: | Size: 908 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot023.png
Normal file
|
After Width: | Height: | Size: 905 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot024.png
Normal file
|
After Width: | Height: | Size: 903 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot025.png
Normal file
|
After Width: | Height: | Size: 866 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot03.png
Normal file
|
After Width: | Height: | Size: 849 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot04.png
Normal file
|
After Width: | Height: | Size: 866 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot05.png
Normal file
|
After Width: | Height: | Size: 866 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot06.png
Normal file
|
After Width: | Height: | Size: 864 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot07.png
Normal file
|
After Width: | Height: | Size: 858 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot08.png
Normal file
|
After Width: | Height: | Size: 875 KiB |
BIN
examples/deployment/modal-example/server/src/assets/robot09.png
Normal file
|
After Width: | Height: | Size: 881 KiB |
197
examples/deployment/modal-example/server/src/bot_gemini.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Gemini Bot Implementation.
|
||||||
|
|
||||||
|
This module implements a chatbot using Google's Gemini Multimodal Live model.
|
||||||
|
It includes:
|
||||||
|
- Real-time audio/video interaction through Daily
|
||||||
|
- Animated robot avatar
|
||||||
|
- Speech-to-speech model
|
||||||
|
|
||||||
|
The bot runs as part of a pipeline that processes audio/video frames and manages
|
||||||
|
the conversation flow using Gemini's streaming capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.audio.vad.vad_analyzer import VADParams
|
||||||
|
from pipecat.frames.frames import (
|
||||||
|
BotStartedSpeakingFrame,
|
||||||
|
BotStoppedSpeakingFrame,
|
||||||
|
Frame,
|
||||||
|
OutputImageRawFrame,
|
||||||
|
SpriteFrame,
|
||||||
|
)
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
|
from pipecat.processors.frameworks.rtvi import RTVIConfig, RTVIObserver, RTVIProcessor
|
||||||
|
from pipecat.services.gemini_multimodal_live.gemini import GeminiMultimodalLiveLLMService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
except ValueError:
|
||||||
|
# Handle the case where logger is already initialized
|
||||||
|
pass
|
||||||
|
|
||||||
|
sprites = []
|
||||||
|
script_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
for i in range(1, 26):
|
||||||
|
# Build the full path to the image file
|
||||||
|
full_path = os.path.join(script_dir, f"assets/robot0{i}.png")
|
||||||
|
# Get the filename without the extension to use as the dictionary key
|
||||||
|
# Open the image and convert it to bytes
|
||||||
|
with Image.open(full_path) as img:
|
||||||
|
sprites.append(OutputImageRawFrame(image=img.tobytes(), size=img.size, format=img.format))
|
||||||
|
|
||||||
|
# Create a smooth animation by adding reversed frames
|
||||||
|
flipped = sprites[::-1]
|
||||||
|
sprites.extend(flipped)
|
||||||
|
|
||||||
|
# Define static and animated states
|
||||||
|
quiet_frame = sprites[0] # Static frame for when bot is listening
|
||||||
|
talking_frame = SpriteFrame(images=sprites) # Animation sequence for when bot is talking
|
||||||
|
|
||||||
|
|
||||||
|
class TalkingAnimation(FrameProcessor):
|
||||||
|
"""Manages the bot's visual animation states.
|
||||||
|
|
||||||
|
Switches between static (listening) and animated (talking) states based on
|
||||||
|
the bot's current speaking status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._is_talking = False
|
||||||
|
|
||||||
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process incoming frames and update animation state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The incoming frame to process
|
||||||
|
direction: The direction of frame flow in the pipeline
|
||||||
|
"""
|
||||||
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
# Switch to talking animation when bot starts speaking
|
||||||
|
if isinstance(frame, BotStartedSpeakingFrame):
|
||||||
|
if not self._is_talking:
|
||||||
|
await self.push_frame(talking_frame)
|
||||||
|
self._is_talking = True
|
||||||
|
# Return to static frame when bot stops speaking
|
||||||
|
elif isinstance(frame, BotStoppedSpeakingFrame):
|
||||||
|
await self.push_frame(quiet_frame)
|
||||||
|
self._is_talking = False
|
||||||
|
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_bot(room_url: str, token: str):
|
||||||
|
"""Main bot execution function.
|
||||||
|
|
||||||
|
Sets up and runs the bot pipeline including:
|
||||||
|
- Daily video transport with specific audio parameters
|
||||||
|
- Gemini Live multimodal model integration
|
||||||
|
- Voice activity detection
|
||||||
|
- Animation processing
|
||||||
|
- RTVI event handling
|
||||||
|
"""
|
||||||
|
# Set up Daily transport with specific audio/video parameters for Gemini
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
token,
|
||||||
|
"Chatbot",
|
||||||
|
DailyParams(
|
||||||
|
audio_out_enabled=True,
|
||||||
|
camera_out_enabled=True,
|
||||||
|
camera_out_width=1024,
|
||||||
|
camera_out_height=576,
|
||||||
|
vad_enabled=True,
|
||||||
|
vad_audio_passthrough=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize the Gemini Multimodal Live model
|
||||||
|
llm = GeminiMultimodalLiveLLMService(
|
||||||
|
api_key=os.getenv("GOOGLE_API_KEY"),
|
||||||
|
voice_id="Puck", # Aoede, Charon, Fenrir, Kore, Puck
|
||||||
|
transcribe_user_audio=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "You are Chatbot, a friendly, helpful robot. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way, but keep your responses brief. Start by introducing yourself.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Set up conversation context and management
|
||||||
|
# The context_aggregator will automatically collect conversation context
|
||||||
|
context = OpenAILLMContext(messages)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
ta = TalkingAnimation()
|
||||||
|
|
||||||
|
#
|
||||||
|
# RTVI events for Pipecat client UI
|
||||||
|
#
|
||||||
|
rtvi = RTVIProcessor(config=RTVIConfig(config=[]))
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
rtvi,
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
ta,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
observers=[RTVIObserver(rtvi)],
|
||||||
|
)
|
||||||
|
await task.queue_frame(quiet_frame)
|
||||||
|
|
||||||
|
@rtvi.event_handler("on_client_ready")
|
||||||
|
async def on_client_ready(rtvi):
|
||||||
|
await rtvi.set_bot_ready()
|
||||||
|
# Kick off the conversation
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_first_participant_joined")
|
||||||
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
await transport.capture_participant_transcription(participant["id"])
|
||||||
|
|
||||||
|
@transport.event_handler("on_participant_left")
|
||||||
|
async def on_participant_left(transport, participant, reason):
|
||||||
|
print(f"Participant left: {participant}")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
225
examples/deployment/modal-example/server/src/bot_openai.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""OpenAI Bot Implementation.
|
||||||
|
|
||||||
|
This module implements a chatbot using OpenAI's GPT-4 model for natural language
|
||||||
|
processing. It includes:
|
||||||
|
- Real-time audio/video interaction through Daily
|
||||||
|
- Animated robot avatar
|
||||||
|
- Text-to-speech using ElevenLabs
|
||||||
|
- Support for both English and Spanish
|
||||||
|
|
||||||
|
The bot runs as part of a pipeline that processes audio/video frames and manages
|
||||||
|
the conversation flow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.frames.frames import (
|
||||||
|
BotStartedSpeakingFrame,
|
||||||
|
BotStoppedSpeakingFrame,
|
||||||
|
Frame,
|
||||||
|
OutputImageRawFrame,
|
||||||
|
SpriteFrame,
|
||||||
|
)
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
|
from pipecat.processors.frameworks.rtvi import RTVIConfig, RTVIObserver, RTVIProcessor
|
||||||
|
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
except ValueError:
|
||||||
|
# Handle the case where logger is already initialized
|
||||||
|
pass
|
||||||
|
|
||||||
|
sprites = []
|
||||||
|
script_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
# Load sequential animation frames
|
||||||
|
for i in range(1, 26):
|
||||||
|
# Build the full path to the image file
|
||||||
|
full_path = os.path.join(script_dir, f"assets/robot0{i}.png")
|
||||||
|
# Get the filename without the extension to use as the dictionary key
|
||||||
|
# Open the image and convert it to bytes
|
||||||
|
with Image.open(full_path) as img:
|
||||||
|
sprites.append(OutputImageRawFrame(image=img.tobytes(), size=img.size, format=img.format))
|
||||||
|
|
||||||
|
# Create a smooth animation by adding reversed frames
|
||||||
|
flipped = sprites[::-1]
|
||||||
|
sprites.extend(flipped)
|
||||||
|
|
||||||
|
# Define static and animated states
|
||||||
|
quiet_frame = sprites[0] # Static frame for when bot is listening
|
||||||
|
talking_frame = SpriteFrame(images=sprites) # Animation sequence for when bot is talking
|
||||||
|
|
||||||
|
|
||||||
|
class TalkingAnimation(FrameProcessor):
|
||||||
|
"""Manages the bot's visual animation states.
|
||||||
|
|
||||||
|
Switches between static (listening) and animated (talking) states based on
|
||||||
|
the bot's current speaking status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._is_talking = False
|
||||||
|
|
||||||
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process incoming frames and update animation state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The incoming frame to process
|
||||||
|
direction: The direction of frame flow in the pipeline
|
||||||
|
"""
|
||||||
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
# Switch to talking animation when bot starts speaking
|
||||||
|
if isinstance(frame, BotStartedSpeakingFrame):
|
||||||
|
if not self._is_talking:
|
||||||
|
await self.push_frame(talking_frame)
|
||||||
|
self._is_talking = True
|
||||||
|
# Return to static frame when bot stops speaking
|
||||||
|
elif isinstance(frame, BotStoppedSpeakingFrame):
|
||||||
|
await self.push_frame(quiet_frame)
|
||||||
|
self._is_talking = False
|
||||||
|
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_bot(room_url: str, token: str):
|
||||||
|
"""Main bot execution function.
|
||||||
|
|
||||||
|
Sets up and runs the bot pipeline including:
|
||||||
|
- Daily video transport
|
||||||
|
- Speech-to-text and text-to-speech services
|
||||||
|
- Language model integration
|
||||||
|
- Animation processing
|
||||||
|
- RTVI event handling
|
||||||
|
"""
|
||||||
|
# Set up Daily transport with video/audio parameters
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
token,
|
||||||
|
"Chatbot",
|
||||||
|
DailyParams(
|
||||||
|
audio_out_enabled=True,
|
||||||
|
camera_out_enabled=True,
|
||||||
|
camera_out_width=1024,
|
||||||
|
camera_out_height=576,
|
||||||
|
vad_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
transcription_enabled=True,
|
||||||
|
#
|
||||||
|
# Spanish
|
||||||
|
#
|
||||||
|
# transcription_settings=DailyTranscriptionSettings(
|
||||||
|
# language="es",
|
||||||
|
# tier="nova",
|
||||||
|
# model="2-general"
|
||||||
|
# )
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize text-to-speech service
|
||||||
|
tts = ElevenLabsTTSService(
|
||||||
|
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||||
|
#
|
||||||
|
# English
|
||||||
|
#
|
||||||
|
voice_id="SAz9YHcvj6GT2YYXdXww",
|
||||||
|
#
|
||||||
|
# Spanish
|
||||||
|
#
|
||||||
|
# model="eleven_multilingual_v2",
|
||||||
|
# voice_id="gD1IexrzCvsXPHUuT0s3",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize LLM service
|
||||||
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
#
|
||||||
|
# English
|
||||||
|
#
|
||||||
|
"content": "You are an incessant one-upper. Start by asking the user how their day is going.",
|
||||||
|
#
|
||||||
|
# Spanish
|
||||||
|
#
|
||||||
|
# "content": "Eres Chatbot, un amigable y útil robot. Tu objetivo es demostrar tus capacidades de una manera breve. Tus respuestas se convertiran a audio así que nunca no debes incluir caracteres especiales. Contesta a lo que el usuario pregunte de una manera creativa, útil y breve. Empieza por presentarte a ti mismo.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Set up conversation context and management
|
||||||
|
# The context_aggregator will automatically collect conversation context
|
||||||
|
context = OpenAILLMContext(messages)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
ta = TalkingAnimation()
|
||||||
|
|
||||||
|
#
|
||||||
|
# RTVI events for Pipecat client UI
|
||||||
|
#
|
||||||
|
rtvi = RTVIProcessor(config=RTVIConfig(config=[]))
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
rtvi,
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
tts,
|
||||||
|
ta,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
observers=[RTVIObserver(rtvi)],
|
||||||
|
)
|
||||||
|
await task.queue_frame(quiet_frame)
|
||||||
|
|
||||||
|
@rtvi.event_handler("on_client_ready")
|
||||||
|
async def on_client_ready(rtvi):
|
||||||
|
await rtvi.set_bot_ready()
|
||||||
|
# Kick off the conversation
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_first_participant_joined")
|
||||||
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
await transport.capture_participant_transcription(participant["id"])
|
||||||
|
|
||||||
|
@transport.event_handler("on_participant_left")
|
||||||
|
async def on_participant_left(transport, participant, reason):
|
||||||
|
print(f"Participant left: {participant}")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
238
examples/deployment/modal-example/server/src/bot_vllm.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""OpenAI Bot Implementation.
|
||||||
|
|
||||||
|
This module implements a chatbot using OpenAI's GPT-4 model for natural language
|
||||||
|
processing. It includes:
|
||||||
|
- Real-time audio/video interaction through Daily
|
||||||
|
- Animated robot avatar
|
||||||
|
- Text-to-speech using ElevenLabs
|
||||||
|
- Support for both English and Spanish
|
||||||
|
|
||||||
|
The bot runs as part of a pipeline that processes audio/video frames and manages
|
||||||
|
the conversation flow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
from openai.types.chat import ChatCompletionMessageParam
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.frames.frames import (
|
||||||
|
BotStartedSpeakingFrame,
|
||||||
|
BotStoppedSpeakingFrame,
|
||||||
|
Frame,
|
||||||
|
OutputImageRawFrame,
|
||||||
|
SpriteFrame,
|
||||||
|
)
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
|
from pipecat.processors.frameworks.rtvi import RTVIConfig, RTVIObserver, RTVIProcessor
|
||||||
|
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
except ValueError:
|
||||||
|
# Handle the case where logger is already initialized
|
||||||
|
pass
|
||||||
|
|
||||||
|
# REPLACE WITH YOUR MODAL URL ENDPOINT
|
||||||
|
modal_url = "https://<Modal workspace>--example-vllm-openai-compatible-serve.modal.run"
|
||||||
|
api_key = os.getenv("VLLM_API_KEY", "super-secret-key")
|
||||||
|
|
||||||
|
|
||||||
|
sprites = []
|
||||||
|
script_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
# Load sequential animation frames
|
||||||
|
for i in range(1, 26):
|
||||||
|
# Build the full path to the image file
|
||||||
|
full_path = os.path.join(script_dir, f"assets/robot0{i}.png")
|
||||||
|
# Get the filename without the extension to use as the dictionary key
|
||||||
|
# Open the image and convert it to bytes
|
||||||
|
with Image.open(full_path) as img:
|
||||||
|
sprites.append(OutputImageRawFrame(image=img.tobytes(), size=img.size, format=img.format))
|
||||||
|
|
||||||
|
# Create a smooth animation by adding reversed frames
|
||||||
|
flipped = sprites[::-1]
|
||||||
|
sprites.extend(flipped)
|
||||||
|
|
||||||
|
# Define static and animated states
|
||||||
|
quiet_frame = sprites[0] # Static frame for when bot is listening
|
||||||
|
talking_frame = SpriteFrame(images=sprites) # Animation sequence for when bot is talking
|
||||||
|
|
||||||
|
|
||||||
|
class TalkingAnimation(FrameProcessor):
|
||||||
|
"""Manages the bot's visual animation states.
|
||||||
|
|
||||||
|
Switches between static (listening) and animated (talking) states based on
|
||||||
|
the bot's current speaking status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._is_talking = False
|
||||||
|
|
||||||
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process incoming frames and update animation state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The incoming frame to process
|
||||||
|
direction: The direction of frame flow in the pipeline
|
||||||
|
"""
|
||||||
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
# Switch to talking animation when bot starts speaking
|
||||||
|
if isinstance(frame, BotStartedSpeakingFrame):
|
||||||
|
if not self._is_talking:
|
||||||
|
await self.push_frame(talking_frame)
|
||||||
|
self._is_talking = True
|
||||||
|
# Return to static frame when bot stops speaking
|
||||||
|
elif isinstance(frame, BotStoppedSpeakingFrame):
|
||||||
|
await self.push_frame(quiet_frame)
|
||||||
|
self._is_talking = False
|
||||||
|
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_bot(room_url: str, token: str):
|
||||||
|
"""Main bot execution function.
|
||||||
|
|
||||||
|
Sets up and runs the bot pipeline including:
|
||||||
|
- Daily video transport
|
||||||
|
- Speech-to-text and text-to-speech services
|
||||||
|
- Language model integration
|
||||||
|
- Animation processing
|
||||||
|
- RTVI event handling
|
||||||
|
"""
|
||||||
|
# Set up Daily transport with video/audio parameters
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
token,
|
||||||
|
"Chatbot",
|
||||||
|
DailyParams(
|
||||||
|
audio_out_enabled=True,
|
||||||
|
camera_out_enabled=True,
|
||||||
|
camera_out_width=1024,
|
||||||
|
camera_out_height=576,
|
||||||
|
vad_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
transcription_enabled=True,
|
||||||
|
#
|
||||||
|
# Spanish
|
||||||
|
#
|
||||||
|
# transcription_settings=DailyTranscriptionSettings(
|
||||||
|
# language="es",
|
||||||
|
# tier="nova",
|
||||||
|
# model="2-general"
|
||||||
|
# )
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize text-to-speech service
|
||||||
|
tts = ElevenLabsTTSService(
|
||||||
|
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||||
|
#
|
||||||
|
# English
|
||||||
|
#
|
||||||
|
voice_id="D38z5RcWu1voky8WS1ja",
|
||||||
|
#
|
||||||
|
# Spanish
|
||||||
|
#
|
||||||
|
# model="eleven_multilingual_v2",
|
||||||
|
# voice_id="gD1IexrzCvsXPHUuT0s3",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize LLM service
|
||||||
|
llm = OpenAILLMService(
|
||||||
|
# To use OpenAI
|
||||||
|
api_key=api_key,
|
||||||
|
# Or, to use a local vLLM (or similar) api server
|
||||||
|
model="neuralmagic/Meta-Llama-3.1-8B-Instruct-quantized.w4a16",
|
||||||
|
base_url=f"{modal_url}/v1",
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
#
|
||||||
|
# English
|
||||||
|
#
|
||||||
|
"content": "You are a salesman for Modal, the cloud-native serverless Python computing platform.",
|
||||||
|
#
|
||||||
|
# Spanish
|
||||||
|
#
|
||||||
|
# "content": "Eres Chatbot, un amigable y útil robot. Tu objetivo es demostrar tus capacidades de una manera breve. Tus respuestas se convertiran a audio así que nunca no debes incluir caracteres especiales. Contesta a lo que el usuario pregunte de una manera creativa, útil y breve. Empieza por presentarte a ti mismo.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Set up conversation context and management
|
||||||
|
# The context_aggregator will automatically collect conversation context
|
||||||
|
context = OpenAILLMContext(messages)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
ta = TalkingAnimation()
|
||||||
|
|
||||||
|
#
|
||||||
|
# RTVI events for Pipecat client UI
|
||||||
|
#
|
||||||
|
rtvi = RTVIProcessor(config=RTVIConfig(config=[]))
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
rtvi,
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
tts,
|
||||||
|
ta,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
observers=[RTVIObserver(rtvi)],
|
||||||
|
)
|
||||||
|
await task.queue_frame(quiet_frame)
|
||||||
|
|
||||||
|
@rtvi.event_handler("on_client_ready")
|
||||||
|
async def on_client_ready(rtvi):
|
||||||
|
await rtvi.set_bot_ready()
|
||||||
|
# Kick off the conversation
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_first_participant_joined")
|
||||||
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
await transport.capture_participant_transcription(participant["id"])
|
||||||
|
|
||||||
|
@transport.event_handler("on_participant_left")
|
||||||
|
async def on_participant_left(transport, participant, reason):
|
||||||
|
print(f"Participant left: {participant}")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
84
examples/deployment/modal-example/server/src/runner.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot_file(arg_bot: str | None) -> str:
|
||||||
|
bot_implementation = arg_bot or os.getenv("BOT_IMPLEMENTATION", "openai").lower().strip()
|
||||||
|
if not bot_implementation:
|
||||||
|
bot_implementation = "openai"
|
||||||
|
if bot_implementation not in ["openai", "gemini", "vllm"]:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid BOT_IMPLEMENTATION: {bot_implementation}. Must be 'openai' or 'gemini'"
|
||||||
|
)
|
||||||
|
return f"bot_{bot_implementation}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_runner(bot_file: str):
|
||||||
|
"""Dynamically import the run_bot function based on the bot name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_name (str): The name of the bot implementation (e.g., 'openai', 'gemini').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
function: The run_bot function from the specified bot module.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ImportError: If the specified bot module or run_bot function is not found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Dynamically construct the module name
|
||||||
|
module_name = f"{bot_file}"
|
||||||
|
# Import the module
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
# Get the run_bot function from the module
|
||||||
|
return getattr(module, "run_bot")
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
raise ImportError(f"Failed to import run_bot from {module_name}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Parse the args to launch the appropriate bot using the given room/token."""
|
||||||
|
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||||
|
parser.add_argument(
|
||||||
|
"-u", "--url", type=str, required=False, help="URL of the Daily room to join"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-t",
|
||||||
|
"--token",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Daily room token",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-b",
|
||||||
|
"--bot",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Bot runner to use (e.g., openai, gemini)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, unknown = parser.parse_known_args()
|
||||||
|
|
||||||
|
url = args.url or os.getenv("DAILY_SAMPLE_ROOM_URL")
|
||||||
|
token = args.token or os.getenv("DAILY_SAMPLE_ROOM_TOKEN")
|
||||||
|
bot_file = get_bot_file(args.bot)
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
raise Exception(
|
||||||
|
"No Daily room specified. use the -u/--url option from the command line, or set DAILY_SAMPLE_ROOM_URL in your environment to specify a Daily room URL."
|
||||||
|
)
|
||||||
|
|
||||||
|
run_bot = get_runner(bot_file)
|
||||||
|
asyncio.run(run_bot(url, token))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -100,7 +100,28 @@ phone numbers with valid values for your use case.
|
|||||||
|
|
||||||
### Dialin Request
|
### Dialin Request
|
||||||
|
|
||||||
The server will receive a request when a call is received from Daily.
|
The server will receive a request when a call is received from Daily.
|
||||||
|
The payload that the webhook received is as follows:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// for dial-in from webhook
|
||||||
|
"To": "+14152251493",
|
||||||
|
"From": "+14158483432",
|
||||||
|
"callId": "string-contains-uuid",
|
||||||
|
"callDomain": "string-contains-uuid",
|
||||||
|
"sipHeaders": {
|
||||||
|
"X-My-Custom-Header": "value",
|
||||||
|
"x-caller": "+1234567890",
|
||||||
|
"x-called": "+1987654321",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The `To`, `From`, `callId`, `callDomain` fields are converted to
|
||||||
|
`snake_case` and mapped to `dialin_settings`. In addition, `sipHeader`
|
||||||
|
contains any custom SIP headers received by Daily on the SIP
|
||||||
|
interconnect address (`sip_uri`). These are headers sent from
|
||||||
|
Twilio or other external SIP platforms, for example, to send the
|
||||||
|
caller's phone number.
|
||||||
|
|
||||||
### Dialout Request
|
### Dialout Request
|
||||||
|
|
||||||
@@ -158,6 +179,7 @@ curl -X POST http://localhost:3000/api/dial \
|
|||||||
"From": "+1987654321",
|
"From": "+1987654321",
|
||||||
"callId": "call-uuid-123",
|
"callId": "call-uuid-123",
|
||||||
"callDomain": "domain-uuid-456",
|
"callDomain": "domain-uuid-456",
|
||||||
|
"sipHeader": {},
|
||||||
"dialout_settings": [
|
"dialout_settings": [
|
||||||
{
|
{
|
||||||
"phoneNumber": "+1234567890",
|
"phoneNumber": "+1234567890",
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ class RoomRequest(BaseModel):
|
|||||||
None, description="A flag to perform voicemail or answeing-machine detection"
|
None, description="A flag to perform voicemail or answeing-machine detection"
|
||||||
)
|
)
|
||||||
call_transfer: Optional[Dict[str, Any]] = Field(None, description="to initiate a call transfer")
|
call_transfer: Optional[Dict[str, Any]] = Field(None, description="to initiate a call transfer")
|
||||||
|
sipHeaders: Optional[Dict[str, Any]] = Field(
|
||||||
|
None,
|
||||||
|
alias="sip_headers",
|
||||||
|
description="Custom SIP headers received from the external SIP provider",
|
||||||
|
)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
populate_by_name = True
|
populate_by_name = True
|
||||||
@@ -57,6 +62,14 @@ class RoomRequest(BaseModel):
|
|||||||
"callDomain": "string-contains-uuid"
|
"callDomain": "string-contains-uuid"
|
||||||
These need to be remapped to dialin_settings
|
These need to be remapped to dialin_settings
|
||||||
|
|
||||||
|
In addition, we may receive in the body that can be
|
||||||
|
sent to the bot as a custom field, sip_headers
|
||||||
|
"sipHeaders": {
|
||||||
|
"X-My-Custom-Header": "value",
|
||||||
|
"x-caller": "+14158483432",
|
||||||
|
"x-called": "+14152251493",
|
||||||
|
},
|
||||||
|
|
||||||
"dialout_settings": [
|
"dialout_settings": [
|
||||||
{"phoneNumber": "+14158483432", "callerId": "+14152251493"},
|
{"phoneNumber": "+14158483432", "callerId": "+14152251493"},
|
||||||
{"sipUri": "sip:username@sip.hostname"}
|
{"sipUri": "sip:username@sip.hostname"}
|
||||||
@@ -157,6 +170,7 @@ async def dial(request: RoomRequest, raw_request: Request):
|
|||||||
"dialout_settings": request.dialout_settings,
|
"dialout_settings": request.dialout_settings,
|
||||||
"voicemail_detection": request.voicemail_detection,
|
"voicemail_detection": request.voicemail_detection,
|
||||||
"call_transfer": request.call_transfer,
|
"call_transfer": request.call_transfer,
|
||||||
|
"sip_headers": request.sipHeaders, # passing the SIP headers to the bot
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"name": "my-daily-app",
|
"name": "my-daily-app",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.11.0",
|
||||||
"next": "^14.0.0",
|
"next": "^14.0.0",
|
||||||
"pino": "^8.15.0",
|
"pino": "^8.15.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -215,10 +215,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.30.tgz",
|
||||||
"integrity": "sha512-vO//GJ/YBco+H7xdQhzJxF7ub3SUwft76jwaeOyVVQFHCi5DCnkP16WHB+JBylo4vOKPoZBlR94Z8xBxNBdNJA==",
|
"integrity": "sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug=="
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
"version": "14.2.25",
|
"version": "14.2.25",
|
||||||
@@ -231,13 +230,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.30.tgz",
|
||||||
"integrity": "sha512-zDJY8gsKEseGAxG+C2hTMT0w9Nk9N1Sk1qV7vXYz9MEiyRoF5ogQX2+vplyUMIfygnjn9/A04I6yrUTRTuRiyQ==",
|
"integrity": "sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -247,13 +245,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.30.tgz",
|
||||||
"integrity": "sha512-U0adH5ryLfmTDkahLwG9sUQG2L0a9rYux8crQeC92rPhi3jGQEY47nByQHrVrt3prZigadwj/2HZ1LUUimuSbg==",
|
"integrity": "sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -263,13 +260,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.30.tgz",
|
||||||
"integrity": "sha512-SINMl1I7UhfHGM7SoRiw0AbwnLEMUnJ/3XXVmhyptzriHbWvPPbbm0OEVG24uUKhuS1t0nvN/DBvm5kz6ZIqpg==",
|
"integrity": "sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -279,13 +275,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.30.tgz",
|
||||||
"integrity": "sha512-s6JaezoyJK2DxrwHWxLWtJKlqKqTdi/zaYigDXUJ/gmx/72CrzdVZfMvUc6VqnZ7YEvRijvYo+0o4Z9DencduA==",
|
"integrity": "sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -295,13 +290,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.30.tgz",
|
||||||
"integrity": "sha512-FEXeUQi8/pLr/XI0hKbe0tgbLmHFRhgXOUiPScz2hk0hSmbGiU8aUqVslj/6C6KA38RzXnWoJXo4FMo6aBxjzg==",
|
"integrity": "sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -311,13 +305,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.30.tgz",
|
||||||
"integrity": "sha512-BUsomaO4d2DuXhXhgQCVt2jjX4B4/Thts8nDoIruEJkhE5ifeQFtvW5c9JkdOtYvE5p2G0hcwQ0UbRaQmQwaVg==",
|
"integrity": "sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -327,13 +320,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.30.tgz",
|
||||||
"integrity": "sha512-5auwsMVzT7wbB2CZXQxDctpWbdEnEW/e66DyXO1DcgHxIyhP06awu+rHKshZE+lPLIGiwtjo7bsyeuubewwxMw==",
|
"integrity": "sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -343,13 +335,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.30.tgz",
|
||||||
"integrity": "sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg==",
|
"integrity": "sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -359,13 +350,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.30.tgz",
|
||||||
"integrity": "sha512-2rdB3T1/Gp7bv1eQTTm9d1Y1sv9UuJ2LAwOE0Pe2prHKe32UNscj7YS13fRB37d0GAiGNR+Y7ZcW8YjDI8Ns0w==",
|
"integrity": "sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -620,11 +610,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -1176,13 +1165,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.8.4",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.4",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1224,11 +1213,10 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -2448,14 +2436,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2614,11 +2603,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -3613,12 +3601,11 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-14.2.30.tgz",
|
||||||
"integrity": "sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw==",
|
"integrity": "sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "14.2.26",
|
"@next/env": "14.2.30",
|
||||||
"@swc/helpers": "0.5.5",
|
"@swc/helpers": "0.5.5",
|
||||||
"busboy": "1.6.0",
|
"busboy": "1.6.0",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
@@ -3633,15 +3620,15 @@
|
|||||||
"node": ">=18.17.0"
|
"node": ">=18.17.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "14.2.26",
|
"@next/swc-darwin-arm64": "14.2.30",
|
||||||
"@next/swc-darwin-x64": "14.2.26",
|
"@next/swc-darwin-x64": "14.2.30",
|
||||||
"@next/swc-linux-arm64-gnu": "14.2.26",
|
"@next/swc-linux-arm64-gnu": "14.2.30",
|
||||||
"@next/swc-linux-arm64-musl": "14.2.26",
|
"@next/swc-linux-arm64-musl": "14.2.30",
|
||||||
"@next/swc-linux-x64-gnu": "14.2.26",
|
"@next/swc-linux-x64-gnu": "14.2.30",
|
||||||
"@next/swc-linux-x64-musl": "14.2.26",
|
"@next/swc-linux-x64-musl": "14.2.30",
|
||||||
"@next/swc-win32-arm64-msvc": "14.2.26",
|
"@next/swc-win32-arm64-msvc": "14.2.30",
|
||||||
"@next/swc-win32-ia32-msvc": "14.2.26",
|
"@next/swc-win32-ia32-msvc": "14.2.30",
|
||||||
"@next/swc-win32-x64-msvc": "14.2.26"
|
"@next/swc-win32-x64-msvc": "14.2.30"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentelemetry/api": "^1.1.0",
|
"@opentelemetry/api": "^1.1.0",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.11.0",
|
||||||
"next": "^14.0.0",
|
"next": "^14.0.0",
|
||||||
"pino": "^8.15.0",
|
"pino": "^8.15.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export default async function handler(req, res) {
|
|||||||
From,
|
From,
|
||||||
callId,
|
callId,
|
||||||
callDomain,
|
callDomain,
|
||||||
|
sipHeaders,
|
||||||
dialout_settings,
|
dialout_settings,
|
||||||
voicemail_detection,
|
voicemail_detection,
|
||||||
call_transfer
|
call_transfer
|
||||||
@@ -102,7 +103,7 @@ export default async function handler(req, res) {
|
|||||||
const sip_config = {
|
const sip_config = {
|
||||||
display_name: From,
|
display_name: From,
|
||||||
sip_mode: 'dial-in',
|
sip_mode: 'dial-in',
|
||||||
num_endpoints: call_transfer !== null ? 2 : 1,
|
num_endpoints: (call_transfer !== undefined && call_transfer !== null) ? 2 : 1,
|
||||||
codecs: {"audio": ["OPUS"]},
|
codecs: {"audio": ["OPUS"]},
|
||||||
};
|
};
|
||||||
daily_room_properties.sip = sip_config;
|
daily_room_properties.sip = sip_config;
|
||||||
@@ -117,6 +118,7 @@ export default async function handler(req, res) {
|
|||||||
dialout_settings,
|
dialout_settings,
|
||||||
voicemail_detection,
|
voicemail_detection,
|
||||||
call_transfer,
|
call_transfer,
|
||||||
|
sip_headers: sipHeaders,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -21,44 +22,23 @@ from pipecat.services.cartesia.tts import CartesiaTTSService
|
|||||||
from pipecat.services.openai.llm import OpenAILLMService
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
# Check if we're in local development mode
|
|
||||||
LOCAL_RUN = os.getenv("LOCAL_RUN")
|
|
||||||
if LOCAL_RUN:
|
|
||||||
import asyncio
|
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
try:
|
|
||||||
from local_runner import configure
|
|
||||||
except ImportError:
|
|
||||||
logger.error("Could not import local_runner module. Local development mode may not work.")
|
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
# Check if we're in local development mode
|
||||||
|
LOCAL_RUN = os.getenv("LOCAL_RUN")
|
||||||
|
|
||||||
async def main(room_url: str, token: str):
|
|
||||||
|
async def main(transport: DailyTransport):
|
||||||
"""Main pipeline setup and execution function.
|
"""Main pipeline setup and execution function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
room_url: The Daily room URL
|
transport: The DailyTransport object for the bot
|
||||||
token: The Daily room token
|
|
||||||
"""
|
"""
|
||||||
logger.debug("Starting bot in room: {}", room_url)
|
logger.debug("Starting bot")
|
||||||
|
|
||||||
transport = DailyTransport(
|
|
||||||
room_url,
|
|
||||||
token,
|
|
||||||
"bot",
|
|
||||||
DailyParams(
|
|
||||||
audio_in_enabled=True,
|
|
||||||
audio_out_enabled=True,
|
|
||||||
transcription_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
tts = CartesiaTTSService(
|
tts = CartesiaTTSService(
|
||||||
api_key=os.getenv("CARTESIA_API_KEY"), voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22"
|
api_key=os.getenv("CARTESIA_API_KEY"), voice_id="71a7ad14-091c-4e8e-a314-022ece01c121"
|
||||||
)
|
)
|
||||||
|
|
||||||
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
@@ -87,10 +67,8 @@ async def main(room_url: str, token: str):
|
|||||||
task = PipelineTask(
|
task = PipelineTask(
|
||||||
pipeline,
|
pipeline,
|
||||||
params=PipelineParams(
|
params=PipelineParams(
|
||||||
allow_interruptions=True,
|
|
||||||
enable_metrics=True,
|
enable_metrics=True,
|
||||||
enable_usage_metrics=True,
|
enable_usage_metrics=True,
|
||||||
report_only_initial_ttfb=True,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,7 +90,7 @@ async def main(room_url: str, token: str):
|
|||||||
logger.info("Participant left: {}", participant)
|
logger.info("Participant left: {}", participant)
|
||||||
await task.cancel()
|
await task.cancel()
|
||||||
|
|
||||||
runner = PipelineRunner()
|
runner = PipelineRunner(handle_sigint=False, force_gc=True)
|
||||||
|
|
||||||
await runner.run(task)
|
await runner.run(task)
|
||||||
|
|
||||||
@@ -126,10 +104,25 @@ async def bot(args: DailySessionArguments):
|
|||||||
body: The configuration object from the request body
|
body: The configuration object from the request body
|
||||||
session_id: The session ID for logging
|
session_id: The session ID for logging
|
||||||
"""
|
"""
|
||||||
|
from pipecat.audio.filters.krisp_filter import KrispFilter
|
||||||
|
|
||||||
logger.info(f"Bot process initialized {args.room_url} {args.token}")
|
logger.info(f"Bot process initialized {args.room_url} {args.token}")
|
||||||
|
|
||||||
|
transport = DailyTransport(
|
||||||
|
args.room_url,
|
||||||
|
args.token,
|
||||||
|
"Pipecat Bot",
|
||||||
|
DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_in_filter=None if LOCAL_RUN else KrispFilter(),
|
||||||
|
audio_out_enabled=True,
|
||||||
|
transcription_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await main(args.room_url, args.token)
|
await main(transport)
|
||||||
logger.info("Bot process completed")
|
logger.info("Bot process completed")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error in bot process: {str(e)}")
|
logger.exception(f"Error in bot process: {str(e)}")
|
||||||
@@ -137,18 +130,27 @@ async def bot(args: DailySessionArguments):
|
|||||||
|
|
||||||
|
|
||||||
# Local development functions
|
# Local development functions
|
||||||
async def local_main():
|
async def local_daily():
|
||||||
"""Function for local development testing."""
|
"""Function for local development testing."""
|
||||||
|
from local_runner import configure
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
(room_url, token) = await configure(session)
|
(room_url, token) = await configure(session)
|
||||||
logger.warning("_")
|
transport = DailyTransport(
|
||||||
logger.warning("_")
|
room_url,
|
||||||
logger.warning(f"Talk to your voice agent here: {room_url}")
|
token,
|
||||||
logger.warning("_")
|
"Pipecat Bot",
|
||||||
logger.warning("_")
|
DailyParams(
|
||||||
webbrowser.open(room_url)
|
audio_in_enabled=True,
|
||||||
await main(room_url, token)
|
audio_out_enabled=True,
|
||||||
|
transcription_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await main(transport)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error in local development mode: {e}")
|
logger.exception(f"Error in local development mode: {e}")
|
||||||
|
|
||||||
@@ -156,6 +158,6 @@ async def local_main():
|
|||||||
# Local development entry point
|
# Local development entry point
|
||||||
if LOCAL_RUN and __name__ == "__main__":
|
if LOCAL_RUN and __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
asyncio.run(local_main())
|
asyncio.run(local_daily())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Failed to run in local mode: {e}")
|
logger.exception(f"Failed to run in local mode: {e}")
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
CARTESIA_API_KEY=
|
CARTESIA_API_KEY=
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
|
# Local dev only
|
||||||
|
DAILY_API_KEY=
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper, DailyRoomParams
|
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper, DailyRoomParams
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
agent_name = "my-first-agent"
|
agent_name = "my-first-agent"
|
||||||
image = "your-username/my-first-agent:0.1"
|
image = "your-username/my-first-agent:0.1"
|
||||||
|
image_credentials = "your-dockerhub-creds"
|
||||||
secret_set = "my-first-agent-secrets"
|
secret_set = "my-first-agent-secrets"
|
||||||
|
enable_krisp = true
|
||||||
|
|
||||||
[scaling]
|
[scaling]
|
||||||
min_instances = 0
|
min_instances = 0
|
||||||
|
|||||||