diff --git a/package-lock.json b/package-lock.json index 0bf3212..6754adf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1913,9 +1913,9 @@ } }, "node_modules/@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz", + "integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==", "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -2053,9 +2053,9 @@ ] }, "node_modules/@rollup/pluginutils": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", - "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2089,9 +2089,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", - "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", + "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", "cpu": [ "arm" ], @@ -2103,9 +2103,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", - "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", + "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", "cpu": [ "arm64" ], @@ -2117,9 +2117,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", - "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", + "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", "cpu": [ "arm64" ], @@ -2131,9 +2131,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", - "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", + "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", "cpu": [ "x64" ], @@ -2145,9 +2145,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", - "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", + "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", "cpu": [ "arm64" ], @@ -2159,9 +2159,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", - "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", + "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", "cpu": [ "x64" ], @@ -2173,9 +2173,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", - "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", + "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", "cpu": [ "arm" ], @@ -2187,9 +2187,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", - "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", + "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", "cpu": [ "arm" ], @@ -2201,9 +2201,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", - "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", + "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", "cpu": [ "arm64" ], @@ -2215,9 +2215,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", - "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", + "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", "cpu": [ "arm64" ], @@ -2229,9 +2229,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", - "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", + "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", "cpu": [ "loong64" ], @@ -2243,9 +2243,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", - "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", + "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", "cpu": [ "ppc64" ], @@ -2257,9 +2257,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", - "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", + "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", "cpu": [ "riscv64" ], @@ -2271,9 +2271,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", - "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", + "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", "cpu": [ "s390x" ], @@ -2285,9 +2285,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", - "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", + "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", "cpu": [ "x64" ], @@ -2299,9 +2299,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", - "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", + "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", "cpu": [ "x64" ], @@ -2313,9 +2313,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", - "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", + "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", "cpu": [ "arm64" ], @@ -2327,9 +2327,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", - "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", + "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", "cpu": [ "ia32" ], @@ -2341,9 +2341,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", - "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", + "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", "cpu": [ "x64" ], @@ -3152,9 +3152,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "dev": true, "funding": [ { @@ -3172,9 +3172,9 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { @@ -3245,6 +3245,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3284,9 +3300,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001687", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz", - "integrity": "sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true, "funding": [ { @@ -3888,12 +3904,12 @@ } }, "node_modules/dunder-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", - "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" }, @@ -3925,9 +3941,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.73", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", - "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==", + "version": "1.5.76", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz", + "integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==", "dev": true, "license": "ISC" }, @@ -3987,9 +4003,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "license": "MIT" }, @@ -4465,9 +4481,9 @@ "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "dev": true, "license": "ISC", "dependencies": { @@ -5278,9 +5294,9 @@ "license": "MIT" }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -6178,12 +6194,13 @@ "license": "MIT" }, "node_modules/json-stable-stringify": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz", - "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz", + "integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" @@ -6772,9 +6789,9 @@ "license": "MIT" }, "node_modules/magic-string": { - "version": "0.30.15", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz", - "integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "license": "MIT", "dependencies": { @@ -6851,9 +6868,9 @@ } }, "node_modules/math-intrinsics": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", - "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7172,12 +7189,12 @@ } }, "node_modules/otpauth": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.5.tgz", - "integrity": "sha512-jQyqOuQExeIl4YIiOUz4TdEcamgAgPX6UYeeS9Iit4lkvs7bwHb0JNDqchGRccbRfvWHV6oRwH36tOsVmc+7hQ==", + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.6.tgz", + "integrity": "sha512-eIcCvuEvcAAPHxUKC9Q4uCe0Fh/yRc5jv9z+f/kvyIF2LPrhgAOuLB7J9CssGYhND/BL8M9hlHBTFmffpoQlMQ==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.5.0" + "@noble/hashes": "1.6.1" }, "funding": { "url": "https://github.com/hectorm/otpauth?sponsor=1" @@ -7839,19 +7856,22 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8011,9 +8031,9 @@ } }, "node_modules/rollup": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", - "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", + "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", "dev": true, "license": "MIT", "dependencies": { @@ -8027,25 +8047,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.28.1", - "@rollup/rollup-android-arm64": "4.28.1", - "@rollup/rollup-darwin-arm64": "4.28.1", - "@rollup/rollup-darwin-x64": "4.28.1", - "@rollup/rollup-freebsd-arm64": "4.28.1", - "@rollup/rollup-freebsd-x64": "4.28.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", - "@rollup/rollup-linux-arm-musleabihf": "4.28.1", - "@rollup/rollup-linux-arm64-gnu": "4.28.1", - "@rollup/rollup-linux-arm64-musl": "4.28.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", - "@rollup/rollup-linux-riscv64-gnu": "4.28.1", - "@rollup/rollup-linux-s390x-gnu": "4.28.1", - "@rollup/rollup-linux-x64-gnu": "4.28.1", - "@rollup/rollup-linux-x64-musl": "4.28.1", - "@rollup/rollup-win32-arm64-msvc": "4.28.1", - "@rollup/rollup-win32-ia32-msvc": "4.28.1", - "@rollup/rollup-win32-x64-msvc": "4.28.1", + "@rollup/rollup-android-arm-eabi": "4.29.1", + "@rollup/rollup-android-arm64": "4.29.1", + "@rollup/rollup-darwin-arm64": "4.29.1", + "@rollup/rollup-darwin-x64": "4.29.1", + "@rollup/rollup-freebsd-arm64": "4.29.1", + "@rollup/rollup-freebsd-x64": "4.29.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", + "@rollup/rollup-linux-arm-musleabihf": "4.29.1", + "@rollup/rollup-linux-arm64-gnu": "4.29.1", + "@rollup/rollup-linux-arm64-musl": "4.29.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", + "@rollup/rollup-linux-riscv64-gnu": "4.29.1", + "@rollup/rollup-linux-s390x-gnu": "4.29.1", + "@rollup/rollup-linux-x64-gnu": "4.29.1", + "@rollup/rollup-linux-x64-musl": "4.29.1", + "@rollup/rollup-win32-arm64-msvc": "4.29.1", + "@rollup/rollup-win32-ia32-msvc": "4.29.1", + "@rollup/rollup-win32-x64-msvc": "4.29.1", "fsevents": "~2.3.2" } }, @@ -8898,9 +8918,9 @@ "license": "0BSD" }, "node_modules/twitter-api-v2": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.18.2.tgz", - "integrity": "sha512-ggImmoAeVgETYqrWeZy+nWnDpwgTP+IvFEc03Pitt1HcgMX+Yw17rP38Fb5FFTinuyNvS07EPtAfZ184uIyB0A==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.19.0.tgz", + "integrity": "sha512-jfG4aapNPM9+4VxNxn0TXvD8Qj8NmVx6cY0hp5K626uZ41qXPaJz33Djd3y6gfHF/+W29+iZz0Y5qB869d/akA==", "license": "Apache-2.0" }, "node_modules/type-check": { @@ -9002,9 +9022,9 @@ } }, "node_modules/undici": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.1.1.tgz", - "integrity": "sha512-WZkQ6eH9f5ZT93gaIffsbUaDpBwjbpvmMbfaEhOnbdUneurTESeRxwPGwjI28mRFESH3W3e8Togijh37ptOQqA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.2.0.tgz", + "integrity": "sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==", "license": "MIT", "engines": { "node": ">=20.18.1" diff --git a/src/_module.ts b/src/_module.ts index 6a5f206..c31b50c 100644 --- a/src/_module.ts +++ b/src/_module.ts @@ -3,3 +3,11 @@ export { Scraper } from './scraper'; export { SearchMode } from './search'; export type { QueryProfilesResponse, QueryTweetsResponse } from './timeline-v1'; export type { Tweet } from './tweets'; + +export { Space } from './spaces/core/Space' +export { SttTtsPlugin } from './spaces/plugins/SttTtsPlugin' +export { RecordToDiskPlugin } from './spaces/plugins/RecordToDiskPlugin' +export { MonitorAudioPlugin } from './spaces/plugins/MonitorAudioPlugin' +export { IdleMonitorPlugin } from './spaces/plugins/IdleMonitorPlugin' + +export * from './types/spaces' diff --git a/src/spaces/core/JanusAudioSource.ts b/src/spaces/core/JanusAudioSource.ts index ccee7ab..14595b3 100644 --- a/src/spaces/core/JanusAudioSource.ts +++ b/src/spaces/core/JanusAudioSource.ts @@ -1,7 +1,8 @@ // src/core/audio.ts import { EventEmitter } from 'events'; -import { nonstandard } from '@roamhq/wrtc'; +import wrtc from '@roamhq/wrtc'; +const { nonstandard } = wrtc; const { RTCAudioSource, RTCAudioSink } = nonstandard; export class JanusAudioSource extends EventEmitter { diff --git a/src/spaces/core/JanusClient.ts b/src/spaces/core/JanusClient.ts index bc24d4b..b7b4ddd 100644 --- a/src/spaces/core/JanusClient.ts +++ b/src/spaces/core/JanusClient.ts @@ -1,7 +1,8 @@ // src/core/JanusClient.ts import { EventEmitter } from 'events'; -import { RTCPeerConnection, MediaStream } from '@roamhq/wrtc'; +import wrtc from '@roamhq/wrtc'; +const { RTCPeerConnection, MediaStream } = wrtc; import { JanusAudioSink, JanusAudioSource } from './JanusAudioSource'; import type { AudioDataWithUser, TurnServersInfo } from '../types'; @@ -112,6 +113,7 @@ export class JanusClient extends EventEmitter { } const feedId = pub.id; console.log('[JanusClient] found feedId =>', feedId); + this.emit('subscribedSpeaker', { userId, feedId }); // 3) "join" as subscriber with "streams: [{ feed, mid: '0', send: true }]" const joinBody = { @@ -161,14 +163,14 @@ export class JanusClient extends EventEmitter { console.log('[JanusClient] subscriber track =>', evt.track.kind); // TODO: REMOVE DEBUG - console.log( - '[JanusClient] subscriber track => kind=', - evt.track.kind, - 'readyState=', - evt.track.readyState, - 'muted=', - evt.track.muted, - ); + // console.log( + // '[JanusClient] subscriber track => kind=', + // evt.track.kind, + // 'readyState=', + // evt.track.readyState, + // 'muted=', + // evt.track.muted, + // ); const sink = new JanusAudioSink(evt.track); sink.on('audioData', (frame) => { @@ -506,7 +508,8 @@ export class JanusClient extends EventEmitter { } private handleJanusEvent(evt: any) { - console.log('[JanusClient] handleJanusEvent =>', JSON.stringify(evt)); + // TODO: REMOVE DEBUG + // console.log('[JanusClient] handleJanusEvent =>', JSON.stringify(evt)); if (!evt.janus) return; if (evt.janus === 'keepalive') { @@ -550,7 +553,9 @@ export class JanusClient extends EventEmitter { if (!this.pc) return; this.pc.addEventListener('iceconnectionstatechange', () => { - console.log('[JanusClient] ICE state =>', this.pc?.iceConnectionState); + // TODO: REMOVE DEBUG + // console.log('[JanusClient] ICE state =>', this.pc?.iceConnectionState); + if (this.pc?.iceConnectionState === 'failed') { this.emit('error', new Error('ICE connection failed')); } diff --git a/src/spaces/core/Space.ts b/src/spaces/core/Space.ts index 42c50df..7405d79 100644 --- a/src/spaces/core/Space.ts +++ b/src/spaces/core/Space.ts @@ -18,6 +18,7 @@ import type { Plugin, AudioDataWithUser, PluginRegistration, + SpeakerInfo, } from '../types'; import { Scraper } from '../../scraper'; @@ -34,6 +35,7 @@ export class Space extends EventEmitter { private broadcastInfo?: BroadcastCreated; private isInitialized = false; private plugins = new Set(); + private speakers = new Map(); constructor(private readonly scraper: Scraper) { super(); @@ -105,6 +107,22 @@ export class Space extends EventEmitter { // You can store or forward to a plugin, run STT, etc. }); + this.janusClient.on('subscribedSpeaker', ({ userId, feedId }) => { + const speaker = this.speakers.get(userId); + if (!speaker) { + console.log( + '[Space] subscribedSpeaker => speaker not found for userId=', + userId, + ); + return; + } + + speaker.janusParticipantId = feedId; + console.log( + `[Space] updated speaker info => userId=${userId}, feedId=${feedId}`, + ); + }); + // 7) Publish the broadcast console.log('[Space] Publishing broadcast...'); await publishBroadcast({ @@ -171,6 +189,11 @@ export class Space extends EventEmitter { throw new Error('[Space] No auth token available'); } + this.speakers.set(userId, { + userId, + sessionUUID, + }); + // 1) Call the "request/approve" endpoint await this.callApproveEndpoint( this.broadcastInfo, @@ -221,6 +244,111 @@ export class Space extends EventEmitter { console.log('[Space] Speaker approved =>', userId); } + /** + * Removes a speaker (userId) on the Twitter side (audiospace/stream/eject) + * then unsubscribes in Janus if needed. + */ + public async removeSpeaker(userId: string) { + if (!this.isInitialized || !this.broadcastInfo) { + throw new Error('[Space] Not initialized or no broadcastInfo'); + } + if (!this.authToken) { + throw new Error('[Space] No auth token available'); + } + if (!this.janusClient) { + throw new Error('[Space] No Janus client initialized'); + } + + const speaker = this.speakers.get(userId); + if (!speaker) { + throw new Error( + `[Space] removeSpeaker => no speaker found for userId=${userId}`, + ); + } + + const sessionUUID = speaker.sessionUUID; + const janusParticipantId = speaker.janusParticipantId; + console.log(sessionUUID, janusParticipantId, speaker); + if (!sessionUUID || janusParticipantId === undefined) { + throw new Error( + `[Space] removeSpeaker => missing sessionUUID or feedId for userId=${userId}`, + ); + } + + const janusHandleId = this.janusClient.getHandleId(); + const janusSessionId = this.janusClient.getSessionId(); + + if (!janusHandleId || !janusSessionId) { + throw new Error( + `[Space] removeSpeaker => missing Janus handle or sessionId for userId=${userId}`, + ); + } + + // 1) Call the Twitter eject endpoint + await this.callRemoveEndpoint( + this.broadcastInfo, + this.authToken, + sessionUUID, + janusParticipantId, + this.broadcastInfo.room_id, + janusHandleId, + janusSessionId, + ); + + // 2) Remove from local speakers map + this.speakers.delete(userId); + + console.log(`[Space] removeSpeaker => removed userId=${userId}`); + } + + /** + * Calls the audiospace/stream/eject endpoint to remove a speaker on Twitter + */ + private async callRemoveEndpoint( + broadcast: BroadcastCreated, + authorizationToken: string, + sessionUUID: string, + janusParticipantId: number, + janusRoomId: string, + webrtcHandleId: number, + webrtcSessionId: number, + ): Promise { + const endpoint = 'https://guest.pscp.tv/api/v1/audiospace/stream/eject'; + + const headers = { + 'Content-Type': 'application/json', + Referer: 'https://x.com/', + Authorization: authorizationToken, + }; + + const body = { + ntpForBroadcasterFrame: '2208988800024000300', + ntpForLiveFrame: '2208988800024000300', + session_uuid: sessionUUID, + chat_token: broadcast.access_token, + janus_room_id: janusRoomId, + janus_participant_id: janusParticipantId, + webrtc_handle_id: webrtcHandleId, + webrtc_session_id: webrtcSessionId, + }; + + console.log('[Space] Removing speaker =>', endpoint, body); + const resp = await fetch(endpoint, { + method: 'POST', + headers, + body: JSON.stringify(body), + }); + + if (!resp.ok) { + const error = await resp.text(); + throw new Error( + `[Space] Failed to remove speaker => ${resp.status}: ${error}`, + ); + } + + console.log('[Space] Speaker removed => sessionUUID=', sessionUUID); + } + pushAudio(samples: Int16Array, sampleRate: number) { this.janusClient?.pushLocalAudio(samples, sampleRate); } @@ -240,34 +368,34 @@ export class Space extends EventEmitter { * Gracefully end the Space (stop broadcast, destroy Janus room, etc.) */ public async finalizeSpace(): Promise { - console.log('[Space] finalizeSpace => stopping broadcast gracefully'); + console.log('[Space] finalizeSpace => stopping broadcast gracefully'); - const tasks: Array> = []; + const tasks: Array> = []; if (this.janusClient) { tasks.push( - this.janusClient.destroyRoom().catch((err) => { - console.error('[Space] destroyRoom error =>', err); - }), + this.janusClient.destroyRoom().catch((err) => { + console.error('[Space] destroyRoom error =>', err); + }), ); } if (this.broadcastInfo) { tasks.push( - this.endAudiospace({ - broadcastId: this.broadcastInfo.room_id, - chatToken: this.broadcastInfo.access_token, - }).catch((err) => { - console.error('[Space] endAudiospace error =>', err); - }), + this.endAudiospace({ + broadcastId: this.broadcastInfo.room_id, + chatToken: this.broadcastInfo.access_token, + }).catch((err) => { + console.error('[Space] endAudiospace error =>', err); + }), ); } if (this.janusClient) { tasks.push( - this.janusClient.leaveRoom().catch((err) => { - console.error('[Space] leaveRoom error =>', err); - }), + this.janusClient.leaveRoom().catch((err) => { + console.error('[Space] leaveRoom error =>', err); + }), ); } @@ -309,6 +437,10 @@ export class Space extends EventEmitter { console.log('[Space] endAudiospace => success =>', json); } + public getSpeakers(): SpeakerInfo[] { + return Array.from(this.speakers.values()); + } + async stop() { console.log('[Space] Stopping...'); diff --git a/src/spaces/plugins/IdleMonitorPlugin.ts b/src/spaces/plugins/IdleMonitorPlugin.ts new file mode 100644 index 0000000..83b53fb --- /dev/null +++ b/src/spaces/plugins/IdleMonitorPlugin.ts @@ -0,0 +1,81 @@ +// src/plugins/IdleMonitorPlugin.ts + +import { Plugin, AudioDataWithUser } from '../types'; +import { Space } from '../core/Space'; + +/** + * Plugin that tracks the last speaker audio timestamp + * and the last local audio timestamp to detect overall silence. + */ +export class IdleMonitorPlugin implements Plugin { + private space?: Space; + private lastSpeakerAudioMs = Date.now(); + private lastLocalAudioMs = Date.now(); + private checkInterval?: NodeJS.Timeout; + + /** + * @param idleTimeoutMs How many ms of silence before triggering idle (default 60s) + * @param checkEveryMs Interval for checking silence (default 10s) + */ + constructor( + private idleTimeoutMs: number = 60_000, + private checkEveryMs: number = 10_000, + ) {} + + onAttach(space: Space) { + this.space = space; + console.log('[IdleMonitorPlugin] onAttach => plugin attached'); + } + + init(params: { space: Space; pluginConfig?: Record }): void { + this.space = params.space; + console.log('[IdleMonitorPlugin] init => setting up idle checks'); + + // Update lastSpeakerAudioMs on incoming speaker audio + this.space.on('audioDataFromSpeaker', (data: AudioDataWithUser) => { + this.lastSpeakerAudioMs = Date.now(); + }); + + // Patch space.pushAudio to update lastLocalAudioMs + const originalPushAudio = this.space.pushAudio.bind(this.space); + this.space.pushAudio = (samples, sampleRate) => { + this.lastLocalAudioMs = Date.now(); + originalPushAudio(samples, sampleRate); + }; + + // Periodically check for silence + this.checkInterval = setInterval(() => this.checkIdle(), this.checkEveryMs); + } + + private checkIdle() { + const now = Date.now(); + const lastAudio = Math.max(this.lastSpeakerAudioMs, this.lastLocalAudioMs); + const idleMs = now - lastAudio; + + if (idleMs >= this.idleTimeoutMs) { + console.log( + '[IdleMonitorPlugin] idleTimeout => no audio for', + idleMs, + 'ms', + ); + this.space?.emit('idleTimeout', { idleMs }); + } + } + + /** + * Returns how many ms have passed since any audio was detected. + */ + public getIdleTimeMs(): number { + const now = Date.now(); + const lastAudio = Math.max(this.lastSpeakerAudioMs, this.lastLocalAudioMs); + return now - lastAudio; + } + + cleanup(): void { + console.log('[IdleMonitorPlugin] cleanup => stopping idle checks'); + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = undefined; + } + } +} diff --git a/src/spaces/plugins/MonitorAudioPlugin.ts b/src/spaces/plugins/MonitorAudioPlugin.ts index ddc6337..34ae5ec 100644 --- a/src/spaces/plugins/MonitorAudioPlugin.ts +++ b/src/spaces/plugins/MonitorAudioPlugin.ts @@ -36,14 +36,14 @@ export class MonitorAudioPlugin implements Plugin { onAudioData(data: AudioDataWithUser): void { // TODO: REMOVE DEBUG - console.log( - '[MonitorAudioPlugin] onAudioData => user=', - data.userId, - 'samples=', - data.samples.length, - 'sampleRate=', - data.sampleRate, - ); + // console.log( + // '[MonitorAudioPlugin] onAudioData => user=', + // data.userId, + // 'samples=', + // data.samples.length, + // 'sampleRate=', + // data.sampleRate, + // ); // Check sampleRate if needed if (!this.ffplay?.stdin.writable) return; diff --git a/src/spaces/plugins/SttTtsPlugin.ts b/src/spaces/plugins/SttTtsPlugin.ts index 4888bdd..63866ec 100644 --- a/src/spaces/plugins/SttTtsPlugin.ts +++ b/src/spaces/plugins/SttTtsPlugin.ts @@ -6,7 +6,6 @@ import { spawn } from 'child_process'; import { Plugin, AudioDataWithUser } from '../types'; import { Space } from '../core/Space'; import { JanusClient } from '../core/JanusClient'; -import OpenAI from 'openai'; interface PluginConfig { openAiApiKey?: string; // for STT & ChatGPT @@ -14,6 +13,13 @@ interface PluginConfig { sttLanguage?: string; // e.g. "en" for Whisper gptModel?: string; // e.g. "gpt-3.5-turbo" silenceThreshold?: number; // amplitude threshold for ignoring silence + voiceId?: string; // specify which ElevenLabs voice to use + elevenLabsModel?: string; // e.g. "eleven_monolingual_v1" + systemPrompt?: string; // ex. "You are a helpful AI assistant" + chatContext?: Array<{ + role: 'system' | 'user' | 'assistant'; + content: string; + }>; } /** @@ -28,11 +34,18 @@ export class SttTtsPlugin implements Plugin { // OpenAI + ElevenLabs private openAiApiKey?: string; - private openAiClient?: OpenAI; private elevenLabsApiKey?: string; private sttLanguage = 'en'; private gptModel = 'gpt-3.5-turbo'; + private voiceId = '21m00Tcm4TlvDq8ikWAM'; + private elevenLabsModel = 'eleven_monolingual_v1'; + + private systemPrompt = 'You are a helpful AI assistant.'; + private chatContext: Array<{ + role: 'system' | 'user' | 'assistant'; + content: string; + }> = []; /** * userId => arrayOfChunks (PCM Int16) @@ -69,13 +82,20 @@ export class SttTtsPlugin implements Plugin { if (typeof config?.silenceThreshold === 'number') { this.silenceThreshold = config.silenceThreshold; } - console.log('[SttTtsPlugin] Plugin config =>', config); + if (config?.voiceId) { + this.voiceId = config.voiceId; + } + if (config?.elevenLabsModel) { + this.voiceId = config.elevenLabsModel; + } - // Create official OpenAI client if we have an API key - if (this.openAiApiKey) { - this.openAiClient = new OpenAI({ apiKey: this.openAiApiKey }); - console.log('[SttTtsPlugin] OpenAI client initialized'); + if (config.systemPrompt) { + this.systemPrompt = config.systemPrompt; + } + if (config.chatContext) { + this.chatContext = config.chatContext; } + console.log('[SttTtsPlugin] Plugin config =>', config); // Listen for mute state changes to trigger STT flush this.space.on( @@ -219,21 +239,53 @@ export class SttTtsPlugin implements Plugin { * OpenAI Whisper STT */ private async transcribeWithOpenAI(wavPath: string, language: string) { - if (!this.openAiClient) { - throw new Error('[SttTtsPlugin] No OpenAI client available'); + if (!this.openAiApiKey) { + throw new Error('[SttTtsPlugin] No OpenAI API key available'); } + try { console.log('[SttTtsPlugin] Transcribe =>', wavPath); - const fileStream = fs.createReadStream(wavPath); - const resp = await this.openAiClient.audio.transcriptions.create({ - file: fileStream, - model: 'whisper-1', - language: language, - temperature: 0, - }); + // Read file into buffer + const fileBuffer = fs.readFileSync(wavPath); + console.log( + '[SttTtsPlugin] File read, size:', + fileBuffer.length, + 'bytes', + ); + + // Create blob from buffer + const blob = new Blob([fileBuffer], { type: 'audio/wav' }); + + // Create FormData + const formData = new FormData(); + formData.append('file', blob, path.basename(wavPath)); + formData.append('model', 'whisper-1'); + formData.append('language', language); + formData.append('temperature', '0'); + + // Call OpenAI API + const response = await fetch( + 'https://api.openai.com/v1/audio/transcriptions', + { + method: 'POST', + headers: { + Authorization: `Bearer ${this.openAiApiKey}`, + }, + body: formData, + }, + ); - const text = resp.text?.trim() || ''; + // Handle errors + if (!response.ok) { + const errorText = await response.text(); + console.error('[SttTtsPlugin] API Error:', errorText); + throw new Error(`OpenAI API error: ${response.status} ${errorText}`); + } + + // Parse response + const data = (await response.json()) as { text: string }; + const text = data.text?.trim() || ''; console.log('[SttTtsPlugin] Transcription =>', text); return text; } catch (err) { @@ -250,6 +302,14 @@ export class SttTtsPlugin implements Plugin { throw new Error('[SttTtsPlugin] No OpenAI API key for ChatGPT'); } const url = 'https://api.openai.com/v1/chat/completions'; + + // Build the final array of messages + const messages = [ + { role: 'system', content: this.systemPrompt }, + ...this.chatContext, + { role: 'user', content: userText }, + ]; + const resp = await fetch(url, { method: 'POST', headers: { @@ -258,20 +318,24 @@ export class SttTtsPlugin implements Plugin { }, body: JSON.stringify({ model: this.gptModel, - messages: [ - { role: 'system', content: 'You are a helpful AI assistant.' }, - { role: 'user', content: userText }, - ], + messages, }), }); + if (!resp.ok) { const errText = await resp.text(); throw new Error( `[SttTtsPlugin] ChatGPT error => ${resp.status} ${errText}`, ); } + const json = await resp.json(); const reply = json.choices?.[0]?.message?.content || ''; + + // Optionally store the conversation in the chatContext + this.chatContext.push({ role: 'user', content: userText }); + this.chatContext.push({ role: 'assistant', content: reply }); + return reply.trim(); } @@ -282,8 +346,7 @@ export class SttTtsPlugin implements Plugin { if (!this.elevenLabsApiKey) { throw new Error('[SttTtsPlugin] No ElevenLabs API key'); } - const voiceId = '21m00Tcm4TlvDq8ikWAM'; // example - const url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`; + const url = `https://api.elevenlabs.io/v1/text-to-speech/${this.voiceId}`; const resp = await fetch(url, { method: 'POST', headers: { @@ -292,6 +355,7 @@ export class SttTtsPlugin implements Plugin { }, body: JSON.stringify({ text, + model_id: this.elevenLabsModel, voice_settings: { stability: 0.4, similarity_boost: 0.8 }, }), }); @@ -373,6 +437,55 @@ export class SttTtsPlugin implements Plugin { } } + public async speakText(text: string): Promise { + // 1) TTS => MP3 + const ttsAudio = await this.elevenLabsTts(text); + + // 2) Convert MP3 -> PCM + const pcm = await this.convertMp3ToPcm(ttsAudio, 48000); + + // 3) Stream to Janus + if (this.janus) { + await this.streamToJanus(pcm, 48000); + console.log('[SttTtsPlugin] speakText => done streaming to space'); + } + } + + /** + * Change the system prompt at runtime. + */ + public setSystemPrompt(prompt: string) { + this.systemPrompt = prompt; + console.log('[SttTtsPlugin] setSystemPrompt =>', prompt); + } + + /** + * Change the GPT model at runtime (e.g. "gpt-4", "gpt-3.5-turbo", etc.). + */ + public setGptModel(model: string) { + this.gptModel = model; + console.log('[SttTtsPlugin] setGptModel =>', model); + } + + /** + * Add a message (system, user or assistant) to the chat context. + * E.g. to store conversation history or inject a persona. + */ + public addMessage(role: 'system' | 'user' | 'assistant', content: string) { + this.chatContext.push({ role, content }); + console.log( + `[SttTtsPlugin] addMessage => role=${role}, content=${content}`, + ); + } + + /** + * Clear the chat context if needed. + */ + public clearChatContext() { + this.chatContext = []; + console.log('[SttTtsPlugin] clearChatContext => done'); + } + cleanup(): void { console.log('[SttTtsPlugin] cleanup => releasing resources'); this.pcmBuffers.clear(); diff --git a/src/spaces/test.ts b/src/spaces/test.ts index ab93281..4f106e3 100644 --- a/src/spaces/test.ts +++ b/src/spaces/test.ts @@ -2,11 +2,11 @@ import 'dotenv/config'; import { Space } from './core/Space'; -import { Scraper } from '../scraper'; // Adjust the path if needed +import { Scraper } from '../scraper'; import { SpaceConfig } from './types'; -import { MonitorAudioPlugin } from './plugins/MonitorAudioPlugin'; import { RecordToDiskPlugin } from './plugins/RecordToDiskPlugin'; import { SttTtsPlugin } from './plugins/SttTtsPlugin'; +import { IdleMonitorPlugin } from './plugins/IdleMonitorPlugin'; /** * Main test entry point @@ -21,98 +21,110 @@ async function main() { process.env.TWITTER_PASSWORD!, ); - // 2) Create Space instance + // 2) Create the Space instance const space = new Space(scraper); - // const monitorPlugin = new MonitorAudioPlugin(1600); - // space.use(monitorPlugin); + // Add a plugin to record audio const recordPlugin = new RecordToDiskPlugin(); space.use(recordPlugin); + + // Create our TTS/STT plugin instance const sttTtsPlugin = new SttTtsPlugin(); space.use(sttTtsPlugin, { openAiApiKey: process.env.OPENAI_API_KEY, elevenLabsApiKey: process.env.ELEVENLABS_API_KEY, + voiceId: 'D38z5RcWu1voky8WS1ja', // example + // You can also initialize systemPrompt, chatContext, etc. here if you wish + // systemPrompt: "You are a calm and friendly AI assistant." + }); + + // Create an IdleMonitorPlugin to stop after 60s of silence + const idlePlugin = new IdleMonitorPlugin(60_000, 10_000); + space.use(idlePlugin); + + // If idle occurs, say goodbye and end the Space + space.on('idleTimeout', async (info) => { + console.log(`[Test] idleTimeout => no audio for ${info.idleMs}ms.`); + await sttTtsPlugin.speakText('Ending Space due to inactivity. Goodbye!'); + await new Promise((r) => setTimeout(r, 10_000)); + await space.stop(); + console.log('[Test] Space stopped due to silence.'); + process.exit(0); }); + // 3) Initialize the Space const config: SpaceConfig = { mode: 'INTERACTIVE', - title: 'Chunked beep test', - description: 'Proper chunked beep to avoid .byteLength error', + title: 'AI Chat - Dynamic GPT Config', + description: 'Space that demonstrates dynamic GPT personalities.', languages: ['en'], }; - - // 3) Initialize the Space const broadcastInfo = await space.initialize(config); const spaceUrl = broadcastInfo.share_url.replace('broadcasts', 'spaces'); - console.log( - '[Test] Space created =>', - spaceUrl, - ); + console.log('[Test] Space created =>', spaceUrl); + // (Optional) Tweet out the Space link await scraper.sendTweet(`${config.title} ${spaceUrl}`); console.log('[Test] Tweet sent'); - // 4) Listen to events - space.on('occupancyUpdate', (upd) => { - console.log( - '[Test] Occupancy =>', - upd.occupancy, - 'participants =>', - upd.totalParticipants, + // --------------------------------------- + // Example of dynamic GPT usage: + // You can change the system prompt at runtime + setTimeout(() => { + console.log('[Test] Changing system prompt to a new persona...'); + sttTtsPlugin.setSystemPrompt( + 'You are a very sarcastic AI who uses short answers.', ); - }); + }, 45_000); + + // Another example: after some time, switch to GPT-4 + setTimeout(() => { + console.log('[Test] Switching GPT model to "gpt-4" (if available)...'); + sttTtsPlugin.setGptModel('gpt-4'); + }, 60_000); + + // Also, demonstrate how to manually call askChatGPT and speak the result + setTimeout(async () => { + console.log('[Test] Asking GPT for an introduction...'); + try { + const response = await sttTtsPlugin['askChatGPT']('Introduce yourself'); + console.log('[Test] ChatGPT introduction =>', response); + + // Then speak it + await sttTtsPlugin.speakText(response); + } catch (err) { + console.error('[Test] askChatGPT error =>', err); + } + }, 75_000); + + // Example: periodically speak a greeting every 60s + setInterval(() => { + sttTtsPlugin + .speakText('Hello everyone, this is an automated greeting.') + .catch((err) => console.error('[Test] speakText() =>', err)); + }, 20_000); + + // 4) Some event listeners space.on('speakerRequest', async (req) => { console.log('[Test] Speaker request =>', req); await space.approveSpeaker(req.userId, req.sessionUUID); + + // Remove the speaker after 10 seconds (testing only) + setTimeout(() => { + console.log( + `[Test] Removing speaker => userId=${req.userId} (after 10s)`, + ); + space.removeSpeaker(req.userId).catch((err) => { + console.error('[Test] removeSpeaker error =>', err); + }); + }, 10_000); }); + space.on('error', (err) => { console.error('[Test] Space Error =>', err); }); - // ================================================== - // BEEP GENERATION (500 ms) @16kHz => 8000 samples - // ================================================== - const beepDurationMs = 500; - const sampleRate = 16000; - const totalSamples = (sampleRate * beepDurationMs) / 1000; // 8000 - const beepFull = new Int16Array(totalSamples); - - // Sine wave: 440Hz, amplitude ~12000 - const freq = 440; - const amplitude = 12000; - for (let i = 0; i < beepFull.length; i++) { - const t = i / sampleRate; - beepFull[i] = amplitude * Math.sin(2 * Math.PI * freq * t); - } - - const FRAME_SIZE = 160; - /** - * Send a beep by slicing beepFull into frames of 160 samples - */ - async function sendBeep() { - console.log('[Test] Starting beep...'); - for (let offset = 0; offset < beepFull.length; offset += FRAME_SIZE) { - // subarray => simple "view" - const portion = beepFull.subarray(offset, offset + FRAME_SIZE); - - // Make a real copy - const frame = new Int16Array(FRAME_SIZE); - frame.set(portion); - - // Now frame.length = 160, and frame.byteLength = 320 - space.pushAudio(frame, sampleRate); - - await new Promise((r) => setTimeout(r, 10)); - } - console.log('[Test] Finished beep'); - } - - // 5) Send beep every 5s - // setInterval(() => { - // sendBeep().catch((err) => console.error('[Test] beep error =>', err)); - // }, 5000); - console.log('[Test] Space is running... press Ctrl+C to exit.'); // Graceful shutdown diff --git a/src/spaces/types.ts b/src/spaces/types.ts index 55a0416..e98f399 100644 --- a/src/spaces/types.ts +++ b/src/spaces/types.ts @@ -41,7 +41,7 @@ export interface BroadcastCreated { broadcast: { user_id: string; twitter_id: string; - media_key: string; // often needed for stream status + media_key: string; }; access_token: string; endpoint: string; @@ -69,7 +69,7 @@ export interface Plugin { */ init?(params: { space: Space; pluginConfig?: Record }): void; - onAudioData(data: AudioDataWithUser): void; + onAudioData?(data: AudioDataWithUser): void; cleanup?(): void; } @@ -77,3 +77,9 @@ export interface PluginRegistration { plugin: Plugin; config?: Record; } + +export interface SpeakerInfo { + userId: string; + sessionUUID: string; + janusParticipantId?: number; +}