From 988de13e1e7fe21e3bbf19bb6b1ccfe09ea66164 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 12 Mar 2026 21:43:21 +0100 Subject: [PATCH] =?UTF-8?q?joel=20beh=C3=B6ver=20en=20python?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 185128 -> 185499 bytes package.json | 1 + src/features/joel/index.ts | 1 + src/features/joel/voice.ts | 194 +++++++++++++++++++++++++++++++++++-- src/index.ts | 3 +- src/web/index.ts | 29 +++++- src/web/oauth.ts | 14 ++- 7 files changed, 226 insertions(+), 16 deletions(-) diff --git a/bun.lockb b/bun.lockb index af0fdb4a4b3b5793e43fbbfc856ad5bd75d677e7..e6ce3f86fa5cb8ab5a102ed7a12c9596bf1e9e43 100755 GIT binary patch delta 27634 zcmeIbd3a9O8aBSymY2MUBm{{dBM~yanIwXUs4WPhh6F(-5fTwe5K|I0&#}~^YKT@1 zEt-~^wZTzp=t=b`$JA7-v{gbWReblehbHaO-|w93obUSn>Au|S-p{k1HIMtaE#YZ<(KWh+AS|#LKlp`d`Z4d1tWsQTA5}-xW`|5-{oIQ=pmed4Bzti7 zY&WTn2Uqb6sBmZU%V>*aaL0b_X{FyMZ5>H2p=LcY{5lUk>(= zq{5;JxN(Id4om?oy4(;<5#B?F$p0T;%D_Hwb8z2BaY-4HG}2L$9H1Wrrskns)h)zLcic3j026{kQ6MP>1~!n5 z08LOFYLYPMiQ9saMo}_SsRz!=rBEoP?(d9TPdm>Z0&T zxdr12Q^scGH9$a$=m9+HgAF+|BRMxG6B$Brn?N6=b4zez$hE-;T6721&=hXi!pw4O5&Oc}{bOU=m6&z0Wy*NS;4K#QoYMdLrKE~6uG*rC@K z<>jIUk=9a#r}pU!ZbV()7dK?E90?HHL#CQ|3rvQag0&J(*5za{b%0J_DtSvV^`wmm zPwn;!nB@1+f2pQy+G}TR9v80c!!lFZnm9PC9qj+9DWCnRf?MTU~ldEA(+N zjiE`olwC<$i|~};3%DnHs~9cAH+pE5^FZfOU<#K%E;S=1qfnBj_0$ZE-gOTFsl-cQ zNSz@OGA4$isgS9adO)TKD)fjB>-xN0->i(uX%p5%rdE2=TPsjzc4|`YXi3@uJ+-1S ztkjI~-E|rjKJTkpj_9YQcm^`1{3~QC@)5|?&g0{>BBkh@HZCtCFC#5?kS^bW9i?FX zvKD>>*cNh1Zf;(|_%!LN9`E?<+&t){>;Y<3buAi~G%lNZv=0hlr8422GybPHtu+LN6|bqZ1#fdp??g5tos_PPHbnUa~mQ-p~l3E+4FuJX1x$Hl_WhWKOK3M`s8R;9;LHUG8d#xRj!rn zUkerrd&)&So!!7x@FXn5$w?CbqMni7ihEdJTT>B0+?E6n9kjG4$!%o&L%L$dv~(Nm%$Y837vOoJf>)au2`h= zWSvuW?x%Ana3e&}R_CTVTj_jnlGeq()cLH=2X)@YnN8Ee?}|06wXdMlA+{-C5Aa}} zL%{Bk9l&nj`!h8@2X=-0zRoMaG#yR^V;@wsZLU_#Ibdq$Nnl&>STHs7N-&CFSQM`Z zhyb^M$3n*ZbFT7;LtRPuvh zs)^UZR6|q2Zs0`Szq9V|32xR69%kH7LU&8F415lz4DAO~!ew9@PsT9BI-Z`Hoh;?9 z(t39G`xhH1W^12I;mK*=MQ!7*ju+myb{fg--Z-_YVEvfSGU`~xOy9et9r&&l;8I6#&cX1 z^B0h?ZWYzyWtovIfHOCWD+X>8UgD;ha_dOaP#)_R&CYT+cg57oT9Oh~c_uG|`~~C@ zsvOFrJQUL!lrKe7VAMtrh0SZ^3w+HI$?Q>RmkUhO0Io z@X9udsTl@ZvMNvGIo=BUr|O1r)>biXKsO$x`qk!c?G)1}jB?HPgetee?4ZRphnK7rX$jCohLPiJL77 z+r{JHe$Q9KjpUVZU*Xoi?GNnEx^fnzuxUI7?g(B2_Z`)J%-w<&Q#W+; zI361m&0ghYkiUlfk}7-is1U_G0o|W6B6Ir=k?dVw5u&hL+$mIH&3IC%VqWMjNnxsg zlwTw}%B!Gj>Yqs>b1NRIs z>8P0fyd?=6>mrGlBJr`@t&@VHWWfEFm%(krAHz-OQJodk^?bP3ctvN$y`zsyTf<#Tylw8B2+tD_Y& zLsETz^XbREyD3cY9Jm+xKDf@Dy`-=~JO=I_UIOWP?`+Ch_fl%lY|@%eCFcm>>1+^L7cKH^C|P@eXZ)CIO?PEGteuY%7HJUm8W!Mr#| zF;5GZB<$pitkgORs3w%LEWZmc@1?N! zxVbk>sIhR>sRCsBe@(cN97*vDyOWTDYZ95|4W+ zlC|ga`zfq|SM*cNAHyd=jgJP$&patkVP1SI+>yL0PBEX0lBBL`JjE>|Sz}(@Utz`k zB;0>;`&e>qfQ4HK|yaetYei3dP?iR0@>|-RUJ-6u) zjVYomp8CyWXsL53e0ydz`p*0Wg)QV235xjywCJ3umz?gA>=&Ligyx;Ca2N8bA&U8% zUXp|?qJAq;Q{KFIsAA4RMil5IPQ86Ew;!gMZDX}bqc)Fpml zkzGrjJ+ASb(F*Ix_rYDmS%$(Yc??`{UII6pUxd4fyNyxIUk}yt%eW;llKJwoF*GAS zhP#VLjaAIohiQFX%M)gav5IL3o-+LS)A(rnL%2hMi%XxSf z%|69hipgdK78ULr7>xzVew@Ni@C3MaycBL%ehcn$Zpl{6S4V1PkvWZ@HoQDrG36y8 zpW5Oueq4Zg7etJHG%-~{6^|RQu;F|)+_k(C?p5xcqnJZej3HbxG15E+5=J<>DGm9p zoJ~;7EmMslQ`|L@jo~HGy#XCf?hVx{f6Co*6?5k_tr|?c)Dp?Y@v>a(h@k7oV>?Eh zy+&y<*X2=N{rSii*4)n5Yz-x~QXREL-r*H_iuopV7^E0UN#i2Te(73!*HUM_QIKeq znbp~>lvm|b4-B8Em_0{pJ|=bi4dN#!Dr_0IpQNy_c)}#iWf{h@t50H-bk^S4@TD zkT<;*!ve5jsetl6l)>s$bVS{jz*X@`V>R(CUQ=%%i>O5VW1Ns<=BV0JEywY{fckqPFt!ek~letp?l4%VsO) zpWs22jp^&9NVDf8Ny3_eCazkc^|vouShERy{v3tv;}vr-B~HPXfiIpD%@TRiTv`Cg z#Zm>g8!w)xuo7~)-TdCHmui>COXn->km{lk3utK}ca`c^s%~2zw~#s$xd(YA+*&+m zk-~baZi(t%;B2wNTJRFM$*Q|qb$=k&KB`zNhpg8{ZC>o!-dqSRZJn&tZSSwVYzY?Q zSMdB`=*(?rNfL&PX52t;>la5s|f*b{EQ1`m3# z>Ne5+sAX(DeM8?w=TM{89EJtuA0msbaZU9{`V(NSzs_*Za z!g=a?Vg~@Tpn8D|jhp{RtfgNwM`{kS(NeGf>#V0wTg?ABL2HB!LEYDC$K_0fz&%_}ZOK`N5rb{Mb%AHRq|&QYft2^y{L8RYtx3 zmu!U?v2v&vG35sHe4(1~Lcj0m=V;Am?FfWSFf5(8Nt9;OMn5bW(gwZNZd~9UQ722NHCy2X@i@U3I-HL-{Eo4_!e_CG*wg z|Ar~U{)mU-57gr$rrRJml)?7RbwwCyan)p*pLwq-HNO-=R($cBE`?;)S+}jp6j@iY z0!QoqHJLKe9eSErd+GkfR4apY`QKn|;`lcV)zS??#FTI%n9>*yhiYXc2|CkllCDqD zISowuQE(c7^T1RO(-~z~y@7&X*`#&fxnzs$KVb?o4-WOBrF!_9OxY~a^);DP%XK}t zkX`~vo1ma>_%@izy8}!SysPtjV5;~%V7mStrg--2;SYi-=pj8kG1bR$`Zs}s=`yj2`)+QEhi?pONaS$?4q4qKL1&V`gG_n2 zt?P+Nz6&N*m98gd{ON8NwGaKI`##Y5A(&!&q|1+W`3aaVV)B2g%Qcx&Q$7DZE>zpX zgxDyq+7zqKbZf24HJK8qr|WAn=^H>#ZA;%aQG5I=T0u`qx7GcLX&&$clPW;h6H|8y2UFG}Wqldz0tH1Br5h5{ZL}`G zh{?Vi{3)WIx?N2s|6b6OSFG+|lS$QA=5_bhRqLQ1WXkgZ-ISPa2kCN6rZSAs^);F5 zC|TFnWXej4u73f`=#JC(y6|`Q+VNd`?Fz{<0}e%(sRtvb?2QLgbUC`dCO3nAp|1Zg zSesh^iwKa7|0W|^`Y$XAO_Iw|LR#zB=_UPNF!iA~5g*m~W~zUx;w?IF1>>Lewl2Rz z0B)&MVrqgvK0(F)|2;wtq0qR9sf~x~{MQo{D&qG~RLICl)9Av^#nzo;jbqsZE*ZMPf|4L{PhH-)z@E7P^brblA@)o=93du z56QH{t4E%HVAB8f1oitTDH>S4^t!D1aQoL-=4Ir8q+xX zgh8yhTcx-lEE){XUI zZN)88ta67U)*T8TvDzJq9v)CUC54}e@qpqHDYkn+5g;CuVv8pfiJnjdimjed#J7UN zrWF*yBB2!&)~%s9M2b*hZVkm=Qe?G;B24Tf#b_@mT6jUxL1cJA;n)U>GoVyPVtt|LDOUSJvC0>Ur=;jDV*H@!;RnTbKPdW$$E0{fibQ`X z`iZUnP;Bvs!X^NU{vsg&iueF14v}JjFk7Iowm^|(fnt!@M~c0qXb}iSyvPWIVss!B zXGk$bI0ZrB7zD-iASi~3lcYF73g2KThKqt=C?*F(afK8kg?9)PZ9^p&`hzyW$>nlDO-7KmF!3q@2kXpz7x=D}>S zs3KY-V!DBric+Fw;xSQ)hr zihV@;g#F8){UU?tfG8(AD4Yf$$rA&R7rwbYXvW1oSVHmTPTQ%78EQPI9HG7r~XPG9}Vb9YP7A>Rj zQKQ)VrbQj_rzrGwO*(~gGX4%PB{RcsOs=*YQvW7Tlx4GXd+ZX~G*;6U(DdWu!>mlX z{qZ*q6p^5a%*J0C?H1)(><)_*W#iZrc0p{D80T&}|IQ$ImSXMCF=z|e4Z!QLq8#WNkZq7$UGDk} zMQ&3q|GrNC(*Lh&K%jTctb5`MYy@!iyDS!-XW_IDefC_tNL>nFGSFXM{w7l^(px#Y zHo|~@fuIMXCFFtYE$HZ$^z`DMt}VKbe%__`Jalb?4*%5OalD{Y_rdfol9colxk5L% z-qCgR17wVTza30A^ds3y-ENoeM{mL@V|4A-bySFKUH1W)@<2bMkJoj3>7^*$+W-@E z;QH0~*)axi(@0UrT$9np36xW{LJ>UC7tHG~wa>yGI<`r~6s zeRBzqzv)W)ZFq-XUB`7@W9WA3x{q~T6X;gxx)adR8;YhtpsqWs+c`pKyaoRhI`YO} zhe)5{UakKM-H?8o-%Lte$(=u8NXXn4N_iBav&bp4?6XPHK<(kw<*L|t$yr833p>$R1x;D6Ps_VYeb>7e=OdNqxI^mhujn;zRWP*eFsBFZyqTz z_5C+E1fVy&6z@GfY$*8yblul=?O%f627xZ)b$J->Z>SncdY~JIL;e<^NFM6CB5F!p z!@d?nuCvx+@+Fp9NF9{smSsQ*uoR%j=tTg{FXO=3Ko&qRKIvuX7=Y#(e}Lu|Z=em( z8ld;j^?>>S{m5em>H>8D`Z1_G@<3Bh51=Qtr@+lhU=^?eSPi@etN~sJ)&lE*4L~VC zbI%(9%{QX}nqz26$pL5@Ne9LNV}UGSGYat$P!7BT%mQWubAb8urN#o>WCK{4)wyJX zxP6&9o5w*+14aq^E6kibOEA(jsU(`r|)*vRhSy8JJ1v81>k#A z`UaN%aDl!vRX_8iKHUnSE=awHdIoiA>cZ6Bs3%c3MNcWD&x6(OL0yGfl?F*`fciFd zGn!P+p@L5WCxFj^3g9!~Q{eBw8Q?5%3OEgX0{jj57pwC2}1N1ow zjSU(zG!SVZ)4=@%I0c*r&HyyO3!iJuv5+Qansna+Xac8&cPl{O`pg3s0@DF{QCkem z0qA!Dn#4aw`y2<30Y?Cu#1{igfTaL-cj_cgllByV=IK-*5%2&o9~VmTxETP@%0Lqw z&HOIFKTu41=Y1471ndKj03QN}fscUwKsj&_H~^#q{ehQ(en1=$4bT@MG@Ew>Xa?^9 zL;w!5==vpVSm=a%`iN^NFbtsi>IOi+cVmWDKS-i=_alJT_+`MS$m3a{0yqPl06qau z0)Gci0p9@}SOzQyrUBCdbUpQx&k^8}Bo4yOV89J<1!zVefJ_Ynh5-pcA}|CP4A6|; z19%BgfNnqxKoht#&>VOlaU1}CLVWjuX1H$&I0H?9=71C6Kr6sMVQ?Av9QXpb2y}*L zCtxP-lYt}xBBpOo`T#uvcYtPe7ueAx?hXV4AwVb)1oVbYFW?63JRy4lZGm<`8^DM9 zmN(EE2m``__CN&S3v>W1Kmb7dxzRudkO_m3KaEIM05gE80Ilp30eag#5P^pRkw90#1!x8!ptK7S z>;{el9|I=<3P%_Tn-ri0;HGm|_>-P=-@?WN^7ph2y#-+&?LlyTd50EL_dpIY4{|Qp zSdM7ucL5>+Kfq|i1F-XAdv;uBH|i8p<5GK4+ffQW0JWVzFb0OyvF3q|HialX0I0pG zt*QN~6H(_01gI}iXQEC;{e%2S*HLHcq@<&`$I}8#k<(&I#f<_e*VIn>!^(4NKbpX( z^H3F2M+ygz0UMoWBjoOosq^#(sN?kldIAd210WrN?x|zN1A}Ndr6Vx)IO=88+o{wV-;tTcf9#{yYY)wDhv zrKMrj3P78y-*i%Mo(SXt)VoVWr`s&O)fwEL2B@j`16zQ%fEB@93@sBLB!71F@-Gzhebhx7MLz9jrzP95;URtj2&xWW zTlk#1ZHkX-{b8sT>)a_=^=c%xSFvAg$y;)F&HXJ^DPQ>CLxRIa%snLN zBBn!fHjZICWqNwocJBvUteC|wL`~@*qKwS%iBn`}9OJgz?Xsw}9KBU-XA#d~5MrGB z=HD#DB=**1J1}%glsFR5NnrB+6mH)E@2RY@Cx9@Y>SP zrmgPT8a+DLFVGJUcVWWzXGCNiw3l$+>2H}I&e^IO1o`0^V2~KwMs~tWhD6xOl9QPH zGwbba9Mtz_+p7!nW4wF99>bE_u8Fut5gI4x&3+PmpBh$bj`2UAm%y{-WC)u$i z-N!gj&}FUsWpKk?-`cTB@UcOdfx_!4@|-U0pE0NA#`%K7YkwZHr>a((T13pjcvANk z%bqev)>2$~%A7>C!#hWThm>MZzhpWUU)*JnG>azINFX8THts7O2^S|S=h$GWl+=8aIG){U$#tb05C zT{1_y==;0jgV9$MV=FE1*SajH+UJaz{v0XYfI&kT41L!+J}qNbCbDAjLr(Db1fmQE zvW>4e{hT>w{A->m4P-jZ6A#x8h$7ti6M5q|ry?DgGc?pFVc^DKyVwz;s{gtO2i8_C z1hzo%X$qfq?uD<qi!p$XJ59h_FwmEF^1^-udadB zL0H+qDnDb#IpwA9_p0MR`Rb?e{{sGY@Sixq$JFZV>?K+(VQABDMJ*-=IX{E}t*TWI ze74Q8Dt$}_;p$>OT*NWiNw)G6nN0qY*@{?Mb~3l{(++v^6WOx72CoY1)k0)mPiSMhRoHVPRv)7TFZ^aM^>_xvvDfq zs_)NTuKdZ8tM=ueA2TYzk@~w;4pt(3!2k8V+ ziI$3}y0U|-g`zQzrCd<9{^Pj2ztmT))Y;~cNHVFp&a$p97IjMuZ)DJk^%b2^9=yP$ z*lMe>wG|7^vY%LLl1+?@UbW#pMWof1gJ`yTt+pIUe73e+^rxZpxV1>dsXDR?zAX5@ z&L7g*BwQ#5heWvbf11TZA_ut^)@Hex(N=1qUQim%6zQW-M)MhAXwRrsL}L@x$tGb_ z7dh%Hw%0TAO8G+-I*9DLayVMQ9r>u`bEvJ>uv$nb@vyEOYGzji zSg`IsS}4d*-PIUJiCT6{z4fc-h8^&+_=RhogR)K%_640T#S!2PSr2-u_`MLN&lVhKCl65*o;(O;6T`-pYR)KD4x>BTg2vHmH+%BHacRoG;dpq?5P! z6q2)XF6qR`Z zW691q$8_?|H&(bjyC0-y234|1tS1BGK-2Ku5sB_L3%*t(QU_F!IM)E#)FaXlE-iX& z-(rxqt>xj%n;+?)nyMiZP?$U z)=U^+=AxeMj$fP`qU`O<7k;oZy7JTNpur-kq1>b;R$Ak@RvW+Iu9XkE#pyPd07mg@{W2;Hy6Fn63e<2L8(BEkcJL51`rxPXSE3@ClIKbAChJmz0oM|MdI~%9BPX6HX zKQ3Hzk4L^OTDw+=sHTYMh8RlpKx834*(ye?bC4bJ=}{>}EwfH6QGD<4TbiO}WAw36 zBB#l3!Hh`sP*cSL3jDiJc)m0@kz)-@J@_=SkYdpa!=8u_Ye?(GeG2gVY_1piO;L$@ z4}T=?xFgfneZ|$L$aG85$pf+Z3wuXUlxRoPN8~^$hC5}*b$NBh$oJ6 z0oK@jCpiLNYm_;`{EVn-j(RnYlHFd~Ei`HRm#e5=G19OPy(6B(F2p#~cJ%ev4e3{L2+89UEH{v7?u<-h=#y%^h;s&Mp~Nim(e9m% z<8UX1{+c#pp>nx8E59XpyE>>Iw-K$67}y+{`CaTx%Z0OXUhmzqNrOf-Jh{C(Vzrin zob>~Ci*o1pc(!Yk=QOo*sbfx$FI+5cf!Z`q#Fa)JK6ZOq4swXeMSZf<44NB9=oVD% zzHlk>;0zdGGN(tKx`V{KF0!w<(?T{m8z=7e*feg(1((`&U>2fo&LlTs+Y&9CA`)97 zJ>wkTlixkOc5hMX=dh#I0mCOkl)wPr5xz~-OFVT!|3MmGwv-)1Q{uEa@JL~w%Is?g zhU>vF2mVQf@IhEV7p!o49Acyk8b&R>gIMZIe*$jI)F*pPkv!CfEl362n=q$uksHmH5^b712zDxXD2l<49roi<}}6 z71&NS$6ALU$?>}~?Z|I#Szvqqd~K`>uk^)IBjRMak$A^VZf@NOuTbj4LIk*?Nb|(^ zZgM;4^-$vNNs)18@v?6(Tyj;w|p-KxgBC;zo;Y*PkU)c)mR5DUV|3MY~q8|3*9kiQ%o}W+BE&#hudso;q#r7*C`gi2V;L z)i|2Cg?pvPM`rhZ@M(|P7=_;}wj;drQT*;o4@LuauCMU1`|_qn+@87A9q?X`G+xLfwz3Kxp6GBi~QB_xF6LG4J~>o;=N3!#OC_LUw6IXGsp$zT7ybLl(d80X7M)2**M`j z=54=z6W^Oo&vW|EJBP)!c5*up`y6ez_JZaK)#e{J;p>B#j5D1#?`J7%YrnHoPZ323 z6-h8ahnq@v#+lIeUa|z8!cdoVId-*wq9|#!=20`#Nq3uAOzG+UKlT zix`}Zv!BbqDcRDfsq%BR!97s{1KDb__`y$hY;GJLy>aV|F+WYb^3|!H?xjZ`=p92f8@eA!TN~wqtg18+@;!Jktl@$K72hu zcKQ+6MafNmHQTy4Jgl}mA@0B+^tUs!9~QYxdvhSKHZV@rUgsRJ|J<#VA=N&n0?|1D ztCVpLcmK|RZ`Ztq{pM;5+cELuYM<@}ViCeR8)tiGj@!H?NOPj6D($wh?(lU_@a=GA&${Em%U;2n@_yBLUB)o0WCG7 zHx2rrq~BZPs|{8N`#@Co`=TAl`5f$AVK?vl0dMVkt2Y|ONaDU20fP|Z#P8KMgPh8H z+;3QIV4Ml=X^HcT9B zY1^(eC14`M`&{84gv5&+mx^ZpK$%8M}Ffa~*cXmGbN@<(Cz12Y7(Z!a`27$qk;F@Wl@O>bj@NrtQA7$5UM?M93nN*jkblCH8bMR9m8@ zXf0{&t!k=^R8>>Gyi7PfGpC8(_WQc`ewTlKJumNhpZUyZW`}X+rm>$Zv?|g8qaO8JcXI$GJ9+Q{pp5pt1z4>jom-ipf-|&g5 z&~>_WtVcxgXH54Pj#W{LzQ=7t@VuNt~yq*zgfv_or zE)I&~0?wI}nUS5ZC>QE0N;~-91h)mh0Jeb_OZjwgYxswNJ-}VS-e4=(3w*<3$WKXr z3ETnlMd0>|QdpFV8&3$L!4#pr^gDnl!7DI`!rupz0WX8wfOFU$8hJ1}W(+vTWe7iXL?2&y4#RVkykO|Q~N}Ln^Bkdzzr!ZLI%Y3;HR3{047BdY?N@C^p65l2j~u_k~alY zPg;xk)NTb}^1q1wOEvWnOnPgLuAv zvNtcWcoh0a7(?3zrfg_rQL;mN8UEd1s=r=fO4Sccsg?9L%vk`YdiUvL$U8{=j6A>D zdFba;Q4Fev31I4mqa^Q))Ekg@s_#-by01|c`N=c&yk|~HO(7dR(RS3xZ{WTO_;8ew zmN|yT_A}bJ7no*;Uyq&N8)N2Jl4Gl|XfG0`LT&=n7%-=kfEc4KI)ce~H!!uq=>bNe z4})n8&CVlp6=gNzlffV0p7ehhXc&ABOqF(3@;ETXE67gGNXaNvlVen&uD9VDLTB#@el)wp@&^{^8&-2U5n3FcM6n@I^<`APmnK`M+c~cc-3*^*_ z=CINY;RhNtE36r2XvPmWay$+{<$Mc%D)N5#shuas8$}u|IW0RsBR?Z8Z=m#7LyvO! zafA^+exzYZN?u<6+!<-g$1>d+IeGbzDaoVss_Iviot&LRJ-QtPREJ$h8;zPbD=jlW zO;LJ}G3wL{OyNHvoH~|ih#xO;>{Qqcq107U({c-vGtsIt)@5kZyfGue7>ysn-yHsy ziH3g;nEZJ;nHi~m)AAZWW|*CqHZx}y5}KKooRW`L{Qz>xy~TK=$E2V*R6TNEz`I+m zg^7?+Z_1lJlRAYmFUiOwPYOV3vmhS~J!j~dCGQJARdEoQO15jFQI#oElQXA7pTkcx zi5J2t&!%8%q92DE!{-JVRbN=N9m*{bz%Wsf;<%ST?-MGtxf?%|gQ^3rvQmfEy}Zm7=jzjJ8@1MP~%0 z<)tKNCa0#QWM&j(Ac44aqlVV-jD{Zl%uc=#9h8zWJEu9SP4q&PWOc!L0 zqE6>W8^-uH%Q8yi2&QBnQuBi^!%s#;!*(itIQMPTsqk2ik;rZ^)xpdehISeJRMzz5 zf^-x|NzFAHW2|Je>>-|fPfk|&nMPiP=2|`s`H*G( z!4xN4@-Xxg>Xg&+CR42`5%5!er{zs9$jD4pM$I+m?%rUsr4yLi&qeb6Ifnl$$!8=V z1tZ|le|!8ESkAe zFj4X}%q3J(w42^4n~RNzBNi%3M;itZ#AuoY@KgN!lj5-=H*Bg3bF(OgC5=)}UGo|!XQ>AXR9_}8kNXjUH3>Ri(oPHmXGdDT8GtRjd*!mjcNQI+*v%ToyLyy61eSo72KKJ-9uyhcrx5; zd?(yCyv9SbCe%}uP~^?Hmq#R9&5J!XYb9h@<%;U@%^;PcGQM06AdN=Zalb3hYm?ysncNLH7q*-pD zJ0gj6K0*-Ji{ieKq!>?snlW zT{O$n=*Cm@Sa)$ZNxvJ1(QoJ<)mjakD;Zw{~Ms#(s#Z`csXA_r&)hO&+Ub}w)|4pgE;foEbGxx0z^eBYsM$}2e9{eslR6RL2nuhWd|s? zFSHaET?skoB=gDv{@||$) zyasLo4-eGXZeARyso%Bb9|Xp*VD1v6v230IH=dWleO`CJ;r3w75{gk3&y#|p*%Dp> z|7Y+I(ETJcvpnX5_(nZ#2@ha`SA}cV zZy>}9= zhQiBXL=w06rXtOSd!ARoZOyHHG?u{Q;XcPV!mZ-feKbp>uGH{ZVbLs&=SEVFN8n!L ztgi=RdiCvO4e?hLY#oXk@!YUTwvw0j)vSMoEJBx+b&F(exmT3Nj_?e)CwY04W(~0` z%5dEpyI&+*$D{gb>=G}5YvWb@G;3U-qI5&J%FBC3vbj7tT4NP_XS8Ph4k1__Wk^3B z-d|%Sycq5&UJ2KUyA06SK%M}1CohBhEx!&ooZDkG_7tBB_Y|*)(Ht9Mo*0Cb_@uUB z`m8XD$7>op#5clq;ni@*ao>R&dzI%7M3H(ZN?)j3_10d+Su9n2EZmN~G*+`_hbsym z2GE(Q8uoLqL7Mf)Uu0D2eR=sHjVaU#0ZQ22f^^&qc-(rt(zo!G*7ahzt|goYjhb&HPOZD&3XqRnr^+hjD+)z2^yQvs}Xt>T?t(ibyOS^$^OQ36KOs=0(U-Vk7?FF4>rbd zJzj%av++{MQeYINsBjtuySUeQ&C248Nu@rIkBvmHEg!G3LVgeKyF4mMv))HIwYh`d zX3@MVi54>V2^u@bli@DrJKUr37|v2Pwvore)p%*DW?eYO=uh?Z`RoMuO4F=ALq_)0(}x#^3S>*iniFhP z|41y%QBySQJt<>){q*EjkY$ZC>Xz0KV}UQ{$?2N)E69z-h=!jRuYqhyf-&mr=`%|e zFP^F~e_jc9C3nft*jb(c*T&1>CiClX1-DPrte-t**vxoTU?lV471L<8v`(iLD1N$T z{nL1(?;Cbu`k1a+h9qHlbEkx8>t5WUfT(SIL8R4tf}yMaVlT!=mc|zGSh%NnX_jVr z2pz0@fzep7ys|ZRlxM)j2!tEV@4;QfqjEIs$CHfmsGP=BYhIP3S*9ezR$~z<$_{{m zJ7;KEcx&K};NiI%X{?Elk_3-$6p7-onf6k=FKjM54K(f!?oH@c4X9{XC67lOKahtA&3a`j zDhuJ5w5CS7q-Pit6t-;o<;qJvd#IZ-_>Dy|Sbw7CXjb28MvuYfFFz9NM-^nBLPnEu zecC!&oTgL#Ki4gq4dy%NYHT5|nTr_?Qe(-db*Y9I&(o|ynMQU-%cpaf$1wv-nbE_~ z@Uq7-yWy#GpguaLbNdq*f$-C|0Nc{Bk?anyfGj@Su*+EK*YWuInzaft%9HWZ@JOpQ z$IK0LthG0MRQE=&%;LEVP!7n9zD6^B7tRVb$H!+FdHu409N?ven)Q2#sa_2;bGcWM zW_>N!oET^U_@0+THe{xeWbF=mD~~GHtY1P-1FXKj(6r%Ikd4bT+5!_@MkHI$lb?i1 zkdYqKi|WH`AWO|RyNx+$*Ye_pn5-eE;=-nGkyfR^C~kd@DZm;GAGNcwEttvei#6+B zDRZD^bG!+kTq#@0{YmO9tP+T%=riw59{-f4o}a~^c`Al=;?+;lzR-6G7Mr=)Sa823 z(ae>zr5c;4yRYc(J>8ArrI2&(Ribgvk|BJ5Nq;-+H0E<~k)YIRzsC|Fc6tZNoUVq&OSO9MK59cEjE7^@Rm(~4KF(^w*RS+B8I zcmmw!yllN@nYmI?5_r=3Xv-x_zjxn)zxNr zb6yR9{u=#34>7B7H&)l|%#Uo;EMGs3PS1~RjJEXRvn&l4s6LkM@?zKs? z>|09@PZaLT%i+&nC&Mit;%=0lhUa>j8dmG7XEn=7_{ZsSJbChSnl)b-Bh*1(|Mu`2 z$nHQEs@EqyUh7bg zLgp>yb(!L|mvUk^pojE>3-#MNg8yT`k$+u_bPKWB(y#w@mia6HNk+6jjg*EGQxlAq zJXXrn!)0h0jIO9m7^zpN!+QPzsTQs!9L5CSYU z`t?7_4oDH}gnki|HJIadD==H@j49fvN!%zDVIDviF`4@~d2kU^xLNPSRIDcfy8bJ+ zB7C{7^(a$%tId|c^q?n*!LMI+nGC_u*Gqtbts916tot!6b^k^%T{`mzTU!=(g0BLo z54;c1RhOyWtANJ9MW6w21)ySG1*pER19TCS6*tI(is0JYZ zJ3tYBkp6pM%8+7_f%U=EFPnfVA!j&LKh4M^xe>UX4EK<7VoKjz`iZGv-I#2tZZhCM zVKUfXW?+};5YufS95OIe`g@QN*Dve_0VNPF1Be;l{bEZO67-T%o%!h(JqjtYDAED< zlc9B)N;v>>npg+PaAK;J(bE6lV57R~CO`%yAYsaQJecxGf_J4wnXOHKuo zJPl40a2}Y3!4o8uO#a2vznGef?w^u^|AZ;(5;)YaR>^egG8w)`%Ih+TN(*H`DYzK~ zFM_FK?gCQ{>;_W;dnK2HsVNSE>H1fg(m5jIzYeCTZ^-y@bVF70E|?Tg%7D5|qDmtM>|Z&FT7 z{%^q~x-I3zbX&tP4C&?$IsD>F9(o0T4<9A>4=@GXmHr>egNvBLf0F*XOxZn@@_&V? zxDH5+qTu&Wg?hpbD0N(QnG$X&<#n0lPLNYVt}>x!VCoj`Qok;fej6#T%Va=XRnA9k zr9xe%h@OxWx0m5{nG)y#IT_+B!-;8x`hh9jA51|3k^{+uiSBN9xaz^14i$ z+htPzFR;<<|AHct{kIbS7xiVg`*#eW`Lh%yq>aIIvZVhp>$8wdh`P-SNRX<0JD8f} zCCNL$_^0fW{+G#vt1gpauS&iBk`GF~-(pI?@Q_p>rV^cy{{IbjLHeJ{^d;l>V2=v^ zSN&AtKgs+I=6!b4&k&7=GSx|KELJiz15?$iYQSZIvkbj|VnX z#G?;zwD2s~rH?W#4Szha8O#444{X#M{&-;fPaf20Xi+?xhW>b9`{RKP-GCnG{&--c zf$_%!8!F+C2ev;R*#19$V0$g#jrnQ9;>`w&yI#ymB;k(vik;qUtk~fVfm3@3x{8GM z5R7XN!7&o}32O%k96Lae)d7O;;s^;2lc22+1a^_(1Hn`u2u_nANVs=|pjAf*=68f3 zL{yUCBnkXFLC{0Y?F7M`P7quqLAdbk3_<745UlJBL4-I@f^#J3?F&I~QQ`~1QeOye zk|0uqcYz?R3k1(~fgnm;C&4umWc7p~S{&&K!G^97#C3&WfY{g-f`Q#2ct8S8#CC(= zE(vybgCJJiBf$MoZB#ZD62(FRfxey3a#B~yE2!$Xn z6oNFdF%*Jbp z_JrUV337y$YW*+?+D1T-D>5P=m>L1WX%gfK_g)aR>IK34UJw+BN)ntTfnRS3W{bJK z*;qD5R1wV;zI{ORL^09h;ylq4!X62lFG`3Oh-#uj5#AS6Bua^j#dV@5MN|}Mq1Z^Y zNYoH57P0+6Pl+<3CE^~@QW1|&>#RiVBw8j|f6#J~K(s=X6Ri~10iabPnP{~*LbOJ> z#DGdg21rbep@PPs7+kn(kgpZ^nbKIcPE-=D7hVHFLd+%FAgYKq3g1{n?Hr4!D`OFL zlQ<6ndsf&7fu0j3M9+(AqRk?lx=h$$C_jfT%F4ub5?mue+z<#}5F3X;@kLQXv`xgu zA$VXMf_KLu_$6_V1b0c0G!%lJV&_l@b_|8UX&3}Ai-chij2i~QF%s++*5OEMk4Prk zD~=HD6E5+fa*;vws;D4(O}LK$?H9R32Sg>&LE$wLbV$r4IxMP)jtJjTprfLg=yh?P z=nY{X4LT-Dh~5;{M8`$=7|>gyl&C^nCwf~%jRl<$8;RZ#HAL@<*m0ouL>bZh;vPuc z9mkr82?=Zvs}wsEsGtc{&_tB$lt@UVf+jMjL&p+XIpM6jMtC)9~p|= z8SLz#kCWIFtgb5{8P1Es94u)g@HZFqLpMR$TFpP3dPx*#vu`oU=H{^b>;sWCgIz`g z#^*8*C-YCKdQZfwF*4c4_@r2eL;Gg3{fy6O+gVR3YnuL&vS_Zz&1cJ*@1Y0zYzaf5 z7tdm6ED890-H5X7YLqseIT~e&Cja_5Y@DUdY^YH3jZ|GHa~^Ya|GLQd(-3-f*B@x4 zC`BEM@%e}pgVfqGHfAVZ=UV@x<^MzeKUD()Jt@B7Tk>!F2VM|s{>;J)e|~6OXdz{b zd^|=|>6huJCUlarGU(7t2zs5qk^*pTgN$xTPCpf(>m@0p=PUX=MAuH4KE0CfXvhln zZ=I0qA5dfH2G`3{k={WLl=rWI>0La%CtD}=UXx+;@tq8#Yrm9HA+n|H0GRTlSMfPg zc1YL5TT&`nt`r^yQ_1KX=S(T1kET=#XCNK-l+asX3T_G<2k5GhGI}46pBn1d+fwEV z-w-J~A!YQzcR0PJrRyCjq}Ri{Wp&Xg?qZPK9-8~Hh#MlQXtiW4amnm)yAh%uMO^_rQV;UtSw~bt4r!$ z6x$Bc#b58V{UA4ozfRH7SF z)*bhW(wJ|gEC8~0kdg89QJmu10S~EnQ_2D%qmRW@pj%QFMCI>IF>sk*(t~l2ub2vD zW`0c%!M!JBlrX-m>zRa7I6xPDWT#~4D~G2{?>i|AgX}ASuD?rJIPSj&T2Lb2OW_>- zMHB|YWs&$fTUJO@%4%Q@unM3j(&Ye6D_P)dAQPaEru0EI1EA@}4$zd+8R!J~0QB{` zAK(k63Kr;zV z95Vo#ET#Y%z%(Ee*bbX%a(D}P5?BZ<0u}>Hff67a$N^}=$OY1YsXzvRWm%a{e*l_^ zn=Bv|NCO57_b-@7A%35#P^D63#{dI?NT4qe0pLf-^t)0e8t4xU0AhhbKyRQY5Cv!e zH6nF!>UlKAsL_J}>f+R;I|9_(sMk_2MMo*5A7j%W$0*cOsEhdk)YtWH2mS<=d=hvc zI179XQ~@6WXMhiZ)4&J7DWDQ~2Y3%C2WTRsiEl5k4pM8*%dtupSTq z&4`1m=8P!EC6N#PXe=nB>;V2 zE}04^4=4iW0Qo>L5DN4F!hmp~C*TS=0nPwT$76tzz$k!zZt^ogKTDxuLF0wSAPr&~ zw3VXmm#ljsO|07hnxtuQ-2u?=g_Z!zfG2?PUEl;z0nkLf0$2&G z0%id;QO^P90yHzH0OJ8~fM(#a)Y>C)Ln{KU05p@)4F4(0^D6K*@CI-gr~uvsjseGk zBfwk0>%dW93NQ*74U7Or0t0{upfAt|z%;G&0(t|@05@vw=D4A0xh2pFpx=ol0*?VS zL45_#nm|+X8vre|G!@gLz8d%l2A>8#20jGd2P%P+z!~5J;5zU$uo@@@9tWNP79fB9 zXQdOs6M@md7{C+o0B8yy1Ea$uP@pH!4G0GUfbIZo z;L?Gqz;qxBpx;5f3#>;50tm#tA21GKaljBD9vA@(1%}b8IUF|+fIk6Cf#twcz#^a! zCgl<@M~b| zIMkU20o0)e1JoH~0g@47&5o4-&scyK;E`bJdDH_(0b_vC0O?Qr}WFw^YAn;4Q#rU@P!EP*+Ae1PU|nDR)YKH}EpB z3!rSa1C0SIuoKt;yaG_090m>n2Y~}j5ry9}cgr4la>R4rvPjpXxcvZd{1y(bU18$J zx2$75+#Yhi&BB?OT*F%9HOBlJ=FUD9qJ{;rOX5@w+r<7R((mBS!S{-I_6}=h+lLgK zF$PZEw2X`Dcs$gB_30h#AK)LLSj3q-EZs)yMH48z{>j&qcjWC~qOu8g{~&}b=_39+ zBtKkC{|?E!h|TbYn5V#SN>V6zr5m^ zVy#s$54-CzqP^`(*^URbAveS^O4~dg&;Q(u!?LX{*VZa@5jAAP5Ygy+=$WVM6@56_ z**WCXk81T6i%2Mhn8)$`eesaDKe_nb{#pg|z&_`puODb#v?a1Owyv^jQZM;2#u1HM&eV|A4H_^X~3699$ipzN>Am!ux1AEUQHY zZ}iskuQ|@I4f$FmA*#(hLa+Gzk0Wyq9(tD-Y^c|*b>eHc7W&nOTor!*K-%UxfSr@x^9osg z?)h4Ub|M)H_(<_M>6zyUzO(D9wa1H79c%S+#a<|cn1{}lc28W{@j&lzmHnfo{-&eI zJb-Wc$aTe;)8;os9}e^n^~bJT@8s?_^W?sOXI6jm#Ku7#p%CmJ=#Tw-7h%7v+c21D zpr~r5y44T$?}52Z6hfrlX(gVy%ZAv@!vbII^2PFkSl>ZN0;7bQ$x*O-u-`m2Fk#0x z@AohJaJI_a@w%Wfs%Df3xrY&?h%A5A9j{Cm!e=uN1spcJ%jS@(y+7;YF2FwwlcuO5 z1@p8(w>b-J9kzV=tjZSHjSQ~}j~`*IK{oS{zcAmi<4?TP7VlCgz$_~KKC#zDbr(O*hwm-}pr z%{({obW)IddFz?4pbu@*w}{7nLW1UjgC~~0<(0K|?%!qY&;yrP4Fwh>cK(Dq-6t-R zFJAnEe9cAI2k>1K10JxT5cBxI#XnAR^|D;277Kt;Xj=1tz^2hlU-|IFR=3*J!^Cbx zEu@vZAzE&;Pf@q7i_<2%Vb=7-nDh@Y&n#@*V8_-8!S5Yz%A)YZXN9hLs-g3xO_%Mf zCe3cjM#>QL+(VD&)Gvcw_g`(wG7#c~IOd6lH%2FXasTOmpwGa>V1F_3XVw;9ujc=Z zZf~BBIHCThA%|+}rRm0E3dc6Dv;GGWMKQ09JA2Ws85M&Hh)QZI9wMr^@-tdqzYmhB zVK$F@i2D8Kv9usCz~7FUf2}Bng6yoeUz_emSp)_9+e1Rd8R&%!#iOh{O0w+uk){_u zzr2`g3SAFZIzk#Dq{!>i?B+EeYkJh+F#ix`=DM~R2Sw|8s6lM)y6LLB?Iq~8f^Mv{ z^7QbI<++p!%t3u0U$1s>Dg5H;NPfx7jpQ;}}W2@-0`E+T@#sz`V-!xVM( zE`0W>)F)0DR#63y5jju@$%g_S^olOr?Z4h}@#oI93TqHTW^KKj9pG@Xb(S0JqE`yG zAL1CLy$1?TP$e?*HaE<_b8`u zeVH~7!F>F0>8F>ve&b!6&<}oMm8yQpn8>N8x?7$7jZ>*SMQJ^C6Pqgh9FTsl&>Yk_ zn|b15<=#F=Q%Cjk)VorsKdNzp*bBYin0!&sm@lw@>u!`}TF-$$>>B%U7NUk=@~Dt@Fic5ZMh&Ol z?jedBL)knEbHg`hKd=6~JrBxuWC!Ia!RjOJQNjkYnMY_=zJ1lB)0icio@fx2s95AW zLcu(9b6NT3_v3H>*jQK4=d8WLrJ?F3M19r6Fww2Pz9G`Bac8sGqe*Y9O@mNrtuWp#>6zog(+U$1gQUo>m{E1iGt0Qe5PiTZ(gRH}W?J#?DLA-#E`qzM`~T^u8yB%7!qpTB72Tr2D#BH;@$&s|;Z*zQ_c-nKs@ z#SmjbFi&QkciinuH<#x3WL~I;Az~$>+RP(d=UjVXt;f&b1=T7n5EYbDiMT-IBffWr zO){1|ptb0pllOtg$c<~n~oy$|hSP#>5o^xaT1(bQF~|C>7a^*V}Sq+p&#>*OEYuloD`@lwGapy&!M98h{k@f@lC z^M=*zfBH5!$Y!3gX&%Xkw6J{B&azT?xS?6kiZXwYG;y5Bg3x9jKYKFq>nn*LO~A%B z2$5*#wpNr;tRmsk5_;zGv+gHLtrr*XL-)s)l6t?gMSSa~rrXT(YUdpM{7>gDd5=T( zcBAD_h}`B#=$x3}3UpPhZlSuXKMWLaHAgF$$KIwS9jfn?+s)yPlgZu|Z0vcyG7^pUjL716aN#)V<}f00SH z*o{h(7O|V6x{j|@G;f789<7B^AzEQz%F%U0xO70B{5W*IyV?p{z=`lNFVU4KNW{5= z`igXSL>MeKxg)Veah-fAqHSw+E`CZRTB{NG^+}Bln#aY>)@U&ERNmcX{X>)If4PAg z43jDL)78R@^lC%|$o9{hXLIi((~B1+Hstk4>Zb3OxI~-7fx@Q^R4|K>Z?Z^k2Qngv z?QO6c8u#jtBgE%zP{LnQb|!tpXETo*c8-p@;N1IW7@82fiePkLnQl9A+CyzAR<%{z zJemhH%&?gU9p5gWJ!Yb7-3ykM+(baI~w>KmVa>v#!z^6^v8q6Hmg{yVuK1ojU1R9mUw zX`X7JQM5fIuP9M%E$amVb z>Yq>V=^X5YehABCHGW&GC%nC}+B6npz0r|A5X-$)_h#ma%-41-bN=kJ`dDoW#32f| zh;O`AFRZvu?bWWf(j?Rd>c~9MdCk>xU%0P%XKhpHjcl)uwwb3lH(Tkv`K;xon@y>q zFvsG}qBzkWtCo2zbNdckqSU1Yt7Pqk;DJRSnZ{TQGDc=l8}n4>+xKU04qKcvM#V~{ zKjJChCW}2CFsrf2#uL!0rpaGF-uB>DBuI-HrppTmX$oVXeDB1_ZgvMnWoIC142hSx z-vMRYAiDdY_FfdRKG>ve5U1dy_k&;is8jH}yts}Cd`7hGgjWSc9Wk`Fi%ld-5z`^U zFB8we7h)dcJZkUes%}lcyjI&C%yXO**|Qg7{;`=>@KB?x8Ht(+#ff-I&q&s`2CtCm z>2PTGx3{`~aDtv)v5BYd@r>Bq3A6w2OxE^5WufrujJ7NqXgtpZKiw&4S?pQ7A6X^h z5HQv|!+2J@gyx`~gYv-j(#|Fy^>t$b2*J18qETRa&TIU}ifPX-Yk{XTeVm~(lMq4= z{udulZ(dk0r%(^k-^$2|^^uiNb(kjJ?5wu3ndjQJy7sgG;ME21p)_Gw%+MkBi5l31 zM}S7YXbR)~(xH*Qs+FngbTOfedM(sEgW5y=>xB4Q`WA>*LR5u$O10(cy3p(md*~5G zj=^SG;)$-v_;Qxed4ulYS;Fia*f`Wg4g&|QEk1e+rMS? zJ_1GA>OBPWL&j0U2C@x-KDG0kBQj2?KcA)5m?k3B%rjyb6hh21v(Gs_xrHxJ`9#kV z;~6ji5tZ6+M9F7++Kn4TZ}srVPxW$4JV$B&z71KTifjlG-+=soqlePF93*5O)cqin z?VI7<|1TB;1ytPfSBGLB*t9r%ce%9Bt zvkSy_hq~!;D1@5FSyy~jx}#Z3?H{!Y=7HCtLq0rM_VoUFwIN4lizWeTt5EZZ?5#T& zOuOai^GU6Sd5HE>hgYUfbm=*#HssE1F&=Sk=CRu+8w?jQ*RtNO)o>T91L$F4jiviyd`TD!PK& zm`8^{Zwokj_IgTUt=`c&Vv-#tG*25J(dXl?ZQ8oLRIBk7%xi(xcWT-H`ty5cy#F&rp^-{zK zqqq@bI>=_8U%v9;m2O`}CI{+z`m8lkY=%O}uM^V8W)D4~CC0py@xs<7hh1F<)P_Xh zXY<&H;2Adr^Uij0ECd-E8?AbQ_Q|4#{6_8D%mdQia|mT2sBbTznpiDs)((|KWU5p~HywcT1O6(I=Q7 zedcHk4C!2h?caz!@x45 z#tVBmjK~#(K{oTac$@8wg=L-d57+AL5rt6bbLwg1fp|bSrQ??K=O@)FmR`U^iYd! zyZshVv!8^YR+(niNxR3q(Z_R6UY;NRJBjSx7sl_HaJCnYz)rtn9d!}j;ar@NqVn&JD@X(GI)h&(0q-WIzhxYALziI#f0H!+-_5c6? diff --git a/package.json b/package.json index 3c3375f..cc77a8c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@ai-sdk/openai": "^0.0.13", + "@noble/ciphers": "^1.3.0", "@discordjs/opus": "^0.10.0", "@discordjs/voice": "^0.18.0", "@elysiajs/cors": "^1.4.0", diff --git a/src/features/joel/index.ts b/src/features/joel/index.ts index eb75960..ce2488b 100644 --- a/src/features/joel/index.ts +++ b/src/features/joel/index.ts @@ -7,3 +7,4 @@ export { getRandomMention, getRandomMemberMention } from "./mentions"; export { startSpontaneousMentionsCron, stopSpontaneousMentionsCron } from "./spontaneous-cron"; export { TypingIndicator } from "./typing"; export { personalities, getPersonality, buildStyledPrompt, STYLE_MODIFIERS } from "./personalities"; +export { logVoiceDependencyHealth, speakVoiceover } from "./voice"; diff --git a/src/features/joel/voice.ts b/src/features/joel/voice.ts index fb1ed44..7e467cc 100644 --- a/src/features/joel/voice.ts +++ b/src/features/joel/voice.ts @@ -8,6 +8,7 @@ import { createAudioPlayer, createAudioResource, entersState, + generateDependencyReport, getVoiceConnection, joinVoiceChannel, StreamType, @@ -25,11 +26,89 @@ const logger = createLogger("Features:Joel:Voice"); const MAX_VOICE_TEXT_LENGTH = 800; const PLAYBACK_TIMEOUT_MS = 60_000; const READY_TIMEOUT_MS = 15_000; +const VOICE_DEPENDENCY_REPORT = generateDependencyReport(); + +type VoiceDependencyHealth = { + hasEncryptionLibrary: boolean; + hasFfmpeg: boolean; + hasOpusLibrary: boolean; + report: string; +}; + +type VoiceConnectionResult = { + channelId: string | null; + connection: VoiceConnection | null; + skipReason?: string; +}; + +type VoicePlaybackEvent = { + authorId: string; + audioBytes?: number; + channelId: string | null; + connectionStatus?: string; + durationMs?: number; + errorMessage?: string; + guildId: string; + outcome: "skipped" | "success" | "error"; + playerStarted: boolean; + skipReason?: string; + textLength: number; +}; + +function extractDependencySection(startHeading: string, endHeading?: string): string { + const startToken = `${startHeading}\n`; + const startIndex = VOICE_DEPENDENCY_REPORT.indexOf(startToken); + if (startIndex === -1) { + return ""; + } + + const sectionStart = startIndex + startToken.length; + const endIndex = endHeading + ? VOICE_DEPENDENCY_REPORT.indexOf(`\n${endHeading}`, sectionStart) + : -1; + + return VOICE_DEPENDENCY_REPORT + .slice(sectionStart, endIndex === -1 ? undefined : endIndex) + .trim(); +} + +function hasInstalledDependency(section: string): boolean { + return section + .split("\n") + .some((line) => line.trim().startsWith("-") && !line.includes("not found")); +} + +function getVoiceDependencyHealth(): VoiceDependencyHealth { + const opusSection = extractDependencySection("Opus Libraries", "Encryption Libraries"); + const encryptionSection = extractDependencySection("Encryption Libraries", "FFmpeg"); + + const hasOpusLibrary = hasInstalledDependency(opusSection); + const hasEncryptionLibrary = hasInstalledDependency(encryptionSection); + const hasFfmpeg = /FFmpeg[\s\S]*- version:\s+(?!not found)/.test(VOICE_DEPENDENCY_REPORT) + && VOICE_DEPENDENCY_REPORT.includes("- libopus: yes"); + + return { + hasEncryptionLibrary, + hasFfmpeg, + hasOpusLibrary, + report: VOICE_DEPENDENCY_REPORT, + }; +} + +const voiceDependencyHealth = getVoiceDependencyHealth(); function isAbortError(error: unknown): boolean { return error instanceof Error && error.name === "AbortError"; } +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return typeof error === "string" ? error : "Unknown error"; +} + function resolveMentions(message: Message, content: string): string { let text = content; @@ -88,14 +167,18 @@ function attachConnectionLogging(connection: VoiceConnection, guildId: string, c }); } -async function getOrCreateConnection(message: Message) { +async function getOrCreateConnection(message: Message): Promise { const voiceChannel = message.member?.voice.channel; if (!voiceChannel) { logger.debug("No voice channel for author", { userId: message.author.id, guildId: message.guildId, }); - return null; + return { + channelId: null, + connection: null, + skipReason: "author_not_in_voice_channel", + }; } const me = message.guild.members.me ?? (await message.guild.members.fetchMe()); @@ -105,7 +188,11 @@ async function getOrCreateConnection(message: Message) { guildId: message.guildId, channelId: voiceChannel.id, }); - return null; + return { + channelId: voiceChannel.id, + connection: null, + skipReason: "missing_connect_or_speak_permission", + }; } const existing = getVoiceConnection(message.guildId); @@ -114,7 +201,10 @@ async function getOrCreateConnection(message: Message) { guildId: message.guildId, channelId: voiceChannel.id, }); - return existing; + return { + channelId: voiceChannel.id, + connection: existing, + }; } if (existing) { @@ -140,7 +230,10 @@ async function getOrCreateConnection(message: Message) { guildId: message.guildId, channelId: voiceChannel.id, }); - return connection; + return { + channelId: voiceChannel.id, + connection, + }; } catch (error) { if (isAbortError(error)) { logger.debug("Voice connection ready timeout", { @@ -152,41 +245,103 @@ async function getOrCreateConnection(message: Message) { logger.error("Voice connection failed to become ready", error); } connection.destroy(); - return null; + return { + channelId: voiceChannel.id, + connection: null, + skipReason: isAbortError(error) ? "voice_connection_ready_timeout" : "voice_connection_failed", + }; } } +export function logVoiceDependencyHealth(): void { + const payload = { + hasEncryptionLibrary: voiceDependencyHealth.hasEncryptionLibrary, + hasFfmpeg: voiceDependencyHealth.hasFfmpeg, + hasOpusLibrary: voiceDependencyHealth.hasOpusLibrary, + }; + + if (voiceDependencyHealth.hasEncryptionLibrary && voiceDependencyHealth.hasFfmpeg && voiceDependencyHealth.hasOpusLibrary) { + logger.info("Discord voice dependency health", payload); + return; + } + + logger.warn("Discord voice dependency health degraded", { + ...payload, + report: voiceDependencyHealth.report, + }); +} + export async function speakVoiceover(message: Message, content: string): Promise { + const playbackEvent: VoicePlaybackEvent = { + authorId: message.author.id, + channelId: null, + guildId: message.guildId, + outcome: "skipped", + playerStarted: false, + textLength: 0, + }; + const startedAt = Date.now(); + if (!config.elevenlabs.apiKey || !config.elevenlabs.voiceId) { logger.debug("Voiceover disabled (missing config)"); + playbackEvent.skipReason = "missing_elevenlabs_config"; + playbackEvent.durationMs = Date.now() - startedAt; + logger.info("Voice playback", playbackEvent); + return; + } + + if (!voiceDependencyHealth.hasEncryptionLibrary) { + playbackEvent.skipReason = "missing_voice_encryption_dependency"; + playbackEvent.durationMs = Date.now() - startedAt; + logger.warn("Voice playback skipped", { + ...playbackEvent, + dependencyReport: voiceDependencyHealth.report, + }); return; } const text = sanitizeForVoiceover(message, content); + playbackEvent.textLength = text.length; if (!text) { logger.debug("Voiceover skipped (empty text after sanitize)"); + playbackEvent.skipReason = "empty_text_after_sanitize"; + playbackEvent.durationMs = Date.now() - startedAt; + logger.info("Voice playback", playbackEvent); return; } let connection: VoiceConnection | null = null; try { - connection = await getOrCreateConnection(message); + const connectionResult = await getOrCreateConnection(message); + playbackEvent.channelId = connectionResult.channelId; + connection = connectionResult.connection; + if (!connection) { logger.debug("Voiceover skipped (no connection)", { guildId: message.guildId, authorId: message.author.id, + skipReason: connectionResult.skipReason, }); + playbackEvent.skipReason = connectionResult.skipReason ?? "no_connection"; return; } + logger.info("Voice playback started", { + authorId: message.author.id, + channelId: playbackEvent.channelId, + guildId: message.guildId, + textLength: playbackEvent.textLength, + }); + const voiceover = getVoiceoverService(); logger.debug("Requesting ElevenLabs voiceover", { textLength: text.length }); const audio = await voiceover.generate({ text }); logger.debug("Voiceover audio received", { bytes: audio.length }); + playbackEvent.audioBytes = audio.length; const player = createAudioPlayer(); - const resource = createAudioResource(Readable.from(audio), { + const resource = createAudioResource(Readable.from([audio]), { inputType: StreamType.Arbitrary, }); @@ -196,6 +351,7 @@ export async function speakVoiceover(message: Message, content: string): P player.on(AudioPlayerStatus.Playing, () => { logger.debug("Audio player started", { guildId: message.guildId }); + playbackEvent.playerStarted = true; }); player.on(AudioPlayerStatus.Idle, () => { @@ -205,13 +361,33 @@ export async function speakVoiceover(message: Message, content: string): P connection.subscribe(player); player.play(resource); - await entersState(player, AudioPlayerStatus.Playing, 5_000).catch(() => undefined); + const playingState = await entersState(player, AudioPlayerStatus.Playing, 5_000).catch(() => undefined); + if (!playingState) { + logger.warn("Voice playback did not enter playing state", { + authorId: message.author.id, + channelId: playbackEvent.channelId, + guildId: message.guildId, + playerStarted: playbackEvent.playerStarted, + }); + } await entersState(player, AudioPlayerStatus.Idle, PLAYBACK_TIMEOUT_MS); + playbackEvent.connectionStatus = connection.state.status; + playbackEvent.outcome = "success"; } catch (error) { + playbackEvent.connectionStatus = connection?.state.status; + playbackEvent.errorMessage = getErrorMessage(error); + playbackEvent.outcome = "error"; if (!isAbortError(error)) { logger.error("Voiceover playback failed", error); } } finally { + playbackEvent.durationMs = Date.now() - startedAt; + if (playbackEvent.outcome === "error") { + logger.warn("Voice playback", playbackEvent); + } else { + logger.info("Voice playback", playbackEvent); + } + if (connection && connection.state.status !== VoiceConnectionStatus.Destroyed) { connection.destroy(); } diff --git a/src/index.ts b/src/index.ts index 8d01364..a41adf8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { BotClient } from "./core/client"; import { config } from "./core/config"; import { createLogger } from "./core/logger"; import { registerEvents } from "./events"; -import { stopSpontaneousMentionsCron } from "./features/joel"; +import { logVoiceDependencyHealth, stopSpontaneousMentionsCron } from "./features/joel"; import { buildWebCss, startWebCssWatcher, startWebServer } from "./web"; import { runMigrations } from "./database/migrate"; import type { FSWatcher } from "fs"; @@ -47,6 +47,7 @@ async function main(): Promise { try { // Run database migrations await runMigrations(); + logVoiceDependencyHealth(); await client.login(config.discord.token); diff --git a/src/web/index.ts b/src/web/index.ts index 2c163d8..27ee936 100644 --- a/src/web/index.ts +++ b/src/web/index.ts @@ -31,6 +31,23 @@ const logger = createLogger("Web"); const pendingStates = new Map(); const STATE_EXPIRY_MS = 5 * 60 * 1000; +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return typeof error === "string" ? error : "Unknown error"; +} + +function escapeHtml(value: string): string { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll("\"", """) + .replaceAll("'", "'"); +} + export function createWebServer(client: BotClient) { return new Elysia() .use(html()) @@ -113,8 +130,16 @@ export function createWebServer(client: BotClient) { return new Response(null, { status: 302, headers }); } catch (err) { - logger.error("OAuth callback failed", err); - return htmlResponse("

Authentication failed

", 500); + const errorMessage = getErrorMessage(err); + logger.error("OAuth callback failed", { + codePresent: !!code, + errorMessage, + state, + }); + return htmlResponse( + `

Authentication failed

${escapeHtml(errorMessage)}

`, + 500 + ); } }) .post("/auth/logout", async ({ request }) => { diff --git a/src/web/oauth.ts b/src/web/oauth.ts index 6922b83..ddaab6a 100644 --- a/src/web/oauth.ts +++ b/src/web/oauth.ts @@ -19,6 +19,12 @@ const userGuildsCache = new Map>(); const inFlightUserRequests = new Map>(); const inFlightGuildRequests = new Map>(); +async function throwDiscordApiError(action: string, response: Response): Promise { + const bodyText = (await response.text()).slice(0, 500); + const detail = bodyText ? ` ${bodyText}` : ""; + throw new Error(`${action} failed (${response.status} ${response.statusText}).${detail}`); +} + function getFromCache(cache: Map>, key: string): T | null { const entry = cache.get(key); if (!entry) { @@ -91,7 +97,7 @@ export async function exchangeCode(code: string): Promise { }); if (!response.ok) { - throw new Error(`Failed to exchange code: ${response.statusText}`); + await throwDiscordApiError("OAuth code exchange", response); } return response.json(); @@ -112,7 +118,7 @@ export async function refreshToken(refreshToken: string): Promise }); if (!response.ok) { - throw new Error(`Failed to refresh token: ${response.statusText}`); + await throwDiscordApiError("OAuth token refresh", response); } return response.json(); @@ -137,7 +143,7 @@ export async function getUser(accessToken: string): Promise { }); if (!response.ok) { - throw new Error(`Failed to get user: ${response.statusText}`); + await throwDiscordApiError("Discord get user", response); } const user = await response.json(); @@ -173,7 +179,7 @@ export async function getUserGuilds(accessToken: string): Promise