diff --git a/server/poetry.lock b/server/poetry.lock index 77242591..b0ded524 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -682,36 +682,108 @@ humanfriendly = ">=9.1" [package.extras] cron = ["capturer (>=2.4)"] +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.extras] +toml = ["tomli"] + [[package]] name = "cryptography" -version = "41.0.2" +version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, - {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, - {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, - {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, ] [package.dependencies] @@ -729,35 +801,35 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "ctranslate2" -version = "3.17.1" +version = "3.18.0" description = "Fast inference engine for Transformer models" optional = false python-versions = ">=3.7" files = [ - {file = "ctranslate2-3.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c64543fa2e592a6687441d9fe5aeca7d103cde315906ec0cdd02d0e6b0c1ba03"}, - {file = "ctranslate2-3.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07db74dd7a7a880c0879c219d9bee4a9db3c8025704a83bf872c49238a28756c"}, - {file = "ctranslate2-3.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70f66bd549e36eb81530aa4dc6c242366b96b306c262799b0e6f498ea37bc61c"}, - {file = "ctranslate2-3.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbed718c97dcf5b7455a3e243882bfacbb2474463f694b8a5f653e324506a063"}, - {file = "ctranslate2-3.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:3a4f22fe3fe9071c6ab5c76688d00dec5984d38434ebf73c3b6b7633006e9ec8"}, - {file = "ctranslate2-3.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03b6d0bcfd87995c92df5a754fdaf654767d86753b3b7206d0a066cd14d7211c"}, - {file = "ctranslate2-3.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da34fcd29f6c142c4ea85b1191c8930afa9b145e21b7b32dc04b4dd203468a26"}, - {file = "ctranslate2-3.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a866f8ada0af060af055c82e12855f68613bb630ecf93b403f7ee8477b04bc4"}, - {file = "ctranslate2-3.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bbda04ebc8bf1e0f1ea6de1610f97e5e83ca94cc1915965b67ef1f79e275bf8"}, - {file = "ctranslate2-3.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:202f10b666149420ff1703ac2058a80ac235d2b5c39c3ba68fb80189b07ec3ac"}, - {file = "ctranslate2-3.17.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7128042718425d88f34418bc47dd7eeb5ca2af90a691244476d20765cd3da777"}, - {file = "ctranslate2-3.17.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b9865e6a0f38bb2ebb2b5827c2efefc90167cf10b9a1c17b14c8142f266334"}, - {file = "ctranslate2-3.17.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1200b698dde769904b54cdc9c42a4216d9fdc720649236af81033a8ca7be41d"}, - {file = "ctranslate2-3.17.1-cp37-cp37m-win_amd64.whl", hash = "sha256:71b93ea4da6989dd4366f186961e1503a0c0ac03beac686fdac0594cc1f72edc"}, - {file = "ctranslate2-3.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:770255e03975f27d1803dcb92994e50a15d02a28bda97094710a1fc7fac13e75"}, - {file = "ctranslate2-3.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:df4898fbdf558cb3cc748d71e41463d1bd2f857aa0cc93d182466e18a1f52e43"}, - {file = "ctranslate2-3.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86241ebeb02f1fa3fdd2edc0b2663eb71c314f3c2991b78da7937ee76ac28ee0"}, - {file = "ctranslate2-3.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ff354b756e379ed05077de1d6b77d852bafcacb49520e47752835da3a1a3b2d"}, - {file = "ctranslate2-3.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:500e310bfbd428ecad83df2aa1579661104d946b7175abb63bf0eab669109837"}, - {file = "ctranslate2-3.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d6ffe882a607d48dab3d381fc44a26e7b9b5f2a95ad03cae0b102af62db3eb"}, - {file = "ctranslate2-3.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9327f38998cd19839280e5222698b94aeabd36383f00f2571b07077d9ddbd78c"}, - {file = "ctranslate2-3.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36fdb7ffc56b7af64d4152cf53ce03eace62087bc6326fa5e6b04d2c9c35d83b"}, - {file = "ctranslate2-3.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:360365096d76208d47f4d30556361989b26847f0fec47ef1c44ea9a2ee39af56"}, - {file = "ctranslate2-3.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d1308f698789e9dc2ef01c34fbc81de6b1b8790ff81af58b0449374afb35686f"}, + {file = "ctranslate2-3.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98cafffed52dbf52951a67a7c6437bafb73bba9fcebe49d7801e2954dd3e57c0"}, + {file = "ctranslate2-3.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6930781af049d26d5497bf6b4cb54313fdcf3bee1015b3c34c9ae7767229d13b"}, + {file = "ctranslate2-3.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0397e1d1186000cb290815e1ba5a954199672ebe9216b686374acf118c25e474"}, + {file = "ctranslate2-3.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aefa12d72d2004df6c8e9e9db3418d7bd1435f64934ec8d376eed63f87d32253"}, + {file = "ctranslate2-3.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:eee60220dc2b009f0c492c60350c468d58a515e20d4fb493f3141532635b39f4"}, + {file = "ctranslate2-3.18.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5699777a784bdf9c43e6bd33bdc2deef21f5d6b1987c28eed86f104a380dfa1f"}, + {file = "ctranslate2-3.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02ebaad27a74d1e110a133849ef998e23e60386194bf36e635f3ffa165d54bdb"}, + {file = "ctranslate2-3.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635dbccf262adfe5bbf4d236601ef248525b3fd1cef4c15f21e66c9b56a5334e"}, + {file = "ctranslate2-3.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b463d6f3e3cf5c879f7f4757113b461f3b0947f0d7828c1d3f8649163a3e1f"}, + {file = "ctranslate2-3.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:f5fdbebced7d01d927431b9f523cf999a81d8c241ddbe2f34486554fbe6b1685"}, + {file = "ctranslate2-3.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a530c7a6cf11c2e227cd304bad541652c6698955a0b42b79a1e8f930f4d36d4"}, + {file = "ctranslate2-3.18.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31866fc9f161f844ff8a18dfdb12ee41dcfa071e0ee4406a86fb15073589724a"}, + {file = "ctranslate2-3.18.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ebb525017a992da2dfbc5650e914f8c83e08be3cb3d885b31997be22473b576"}, + {file = "ctranslate2-3.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8020a4dc50ab4d27cda6168e93a85ce3cfb3ef6e5669d33c2e748a2744481aa8"}, + {file = "ctranslate2-3.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed90b047af7ff1762514734c76354c52642923ac70cb557316b846ccb29408d2"}, + {file = "ctranslate2-3.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2f7cc567c1a308cc7718d4055a9348aa5db8deaa95f2a009121641134e29efc8"}, + {file = "ctranslate2-3.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84edc7c0b72b3e3054da6cff97e394714a882043b5aa1684865645270eb1914a"}, + {file = "ctranslate2-3.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415bafd07661ed868a5c73b85274fff2dd5d8bf4f2a69aa00aab7efbd181bff0"}, + {file = "ctranslate2-3.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:4749e93626e6c341b5a5ebf5ed2418718f35d017fb20cd8bd0c7f0dd5e4ae608"}, + {file = "ctranslate2-3.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d67944d40f0295f8d0f102ccd4c122096f01403b1c608b428d96b4d8cff183ae"}, + {file = "ctranslate2-3.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9812be9785316ae15a9c26ef08de5827c530c20fe679bd0242d5e5028adf949"}, + {file = "ctranslate2-3.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e25667acc214ae35cb1ea250e72a6bf11a485fdc644dbafbc8405f61fc1c04"}, + {file = "ctranslate2-3.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d10c2d787fc102ab0f7b0a983fd57cf57b0bc94faba774613066fa277220b68"}, + {file = "ctranslate2-3.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:58882df9c4abc162429beb5adfd3edd57fe5b08be19719d2d6f29a8ad9d869f3"}, ] [package.dependencies] @@ -802,6 +874,38 @@ typing-extensions = ">=4.5.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "fastapi-pagination" +version = "0.12.7" +description = "FastAPI pagination" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "fastapi_pagination-0.12.7-py3-none-any.whl", hash = "sha256:dabf3810343b63841def98862707098f5dac6867c283b118a1b2be4d8bc820df"}, + {file = "fastapi_pagination-0.12.7.tar.gz", hash = "sha256:627e561101c4845a36e1ec1da9d38c967b17dffc760dacacdc5e0e5118cb2334"}, +] + +[package.dependencies] +fastapi = ">=0.93.0" +pydantic = ">=1.9.1" + +[package.extras] +all = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)", "beanie (>=1.11.9,<2.0.0)", "bunnet (>=1.1.0,<2.0.0)", "databases (>=0.6.0)", "django (<5.0.0)", "mongoengine (>=0.23.1,<0.28.0)", "motor (>=2.5.1,<4.0.0)", "orm (>=0.3.1)", "ormar (>=0.11.2)", "piccolo (>=0.89,<0.120)", "pony (>=0.7.16,<0.8.0)", "scylla-driver (>=3.25.6,<4.0.0)", "sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.8,<0.0.9)", "tortoise-orm (>=0.16.18,<0.20.0)"] +asyncpg = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)"] +beanie = ["beanie (>=1.11.9,<2.0.0)"] +bunnet = ["bunnet (>=1.1.0,<2.0.0)"] +databases = ["databases (>=0.6.0)"] +django = ["databases (>=0.6.0)", "django (<5.0.0)"] +mongoengine = ["mongoengine (>=0.23.1,<0.28.0)"] +motor = ["motor (>=2.5.1,<4.0.0)"] +orm = ["databases (>=0.6.0)", "orm (>=0.3.1)"] +ormar = ["ormar (>=0.11.2)"] +piccolo = ["piccolo (>=0.89,<0.120)"] +scylla-driver = ["scylla-driver (>=3.25.6,<4.0.0)"] +sqlalchemy = ["SQLAlchemy (>=1.3.20)", "sqlakeyset (>=2.0.1680321678,<3.0.0)"] +sqlmodel = ["sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.8,<0.0.9)"] +tortoise = ["tortoise-orm (>=0.16.18,<0.20.0)"] + [[package]] name = "faster-whisper" version = "0.7.1" @@ -1137,6 +1241,23 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +[[package]] +name = "httpx-ws" +version = "0.4.1" +description = "WebSockets support for HTTPX" +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx_ws-0.4.1-py3-none-any.whl", hash = "sha256:01bdaeb66add8196485dc39912abd0a3e95b67c244aededc151156ac6adca850"}, + {file = "httpx_ws-0.4.1.tar.gz", hash = "sha256:5f3e291e8fb99c89f994329d883e5679d02a0b5b12a1e414f7f8630c276b6744"}, +] + +[package.dependencies] +anyio = "*" +httpcore = ">=0.17.3,<0.18" +httpx = ">=0.23.1" +wsproto = "*" + [[package]] name = "huggingface-hub" version = "0.16.4" @@ -1358,36 +1479,36 @@ files = [ [[package]] name = "numpy" -version = "1.25.1" +version = "1.25.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77d339465dff3eb33c701430bcb9c325b60354698340229e1dff97745e6b3efa"}, - {file = "numpy-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d736b75c3f2cb96843a5c7f8d8ccc414768d34b0a75f466c05f3a739b406f10b"}, - {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a90725800caeaa160732d6b31f3f843ebd45d6b5f3eec9e8cc287e30f2805bf"}, - {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c6c9261d21e617c6dc5eacba35cb68ec36bb72adcff0dee63f8fbc899362588"}, - {file = "numpy-1.25.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0def91f8af6ec4bb94c370e38c575855bf1d0be8a8fbfba42ef9c073faf2cf19"}, - {file = "numpy-1.25.1-cp310-cp310-win32.whl", hash = "sha256:fd67b306320dcadea700a8f79b9e671e607f8696e98ec255915c0c6d6b818503"}, - {file = "numpy-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1516db588987450b85595586605742879e50dcce923e8973f79529651545b57"}, - {file = "numpy-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b82655dd8efeea69dbf85d00fca40013d7f503212bc5259056244961268b66e"}, - {file = "numpy-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e8f6049c4878cb16960fbbfb22105e49d13d752d4d8371b55110941fb3b17800"}, - {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41a56b70e8139884eccb2f733c2f7378af06c82304959e174f8e7370af112e09"}, - {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5154b1a25ec796b1aee12ac1b22f414f94752c5f94832f14d8d6c9ac40bcca6"}, - {file = "numpy-1.25.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38eb6548bb91c421261b4805dc44def9ca1a6eef6444ce35ad1669c0f1a3fc5d"}, - {file = "numpy-1.25.1-cp311-cp311-win32.whl", hash = "sha256:791f409064d0a69dd20579345d852c59822c6aa087f23b07b1b4e28ff5880fcb"}, - {file = "numpy-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:c40571fe966393b212689aa17e32ed905924120737194b5d5c1b20b9ed0fb171"}, - {file = "numpy-1.25.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d7abcdd85aea3e6cdddb59af2350c7ab1ed764397f8eec97a038ad244d2d105"}, - {file = "numpy-1.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a180429394f81c7933634ae49b37b472d343cccb5bb0c4a575ac8bbc433722f"}, - {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d412c1697c3853c6fc3cb9751b4915859c7afe6a277c2bf00acf287d56c4e625"}, - {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20e1266411120a4f16fad8efa8e0454d21d00b8c7cee5b5ccad7565d95eb42dd"}, - {file = "numpy-1.25.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f76aebc3358ade9eacf9bc2bb8ae589863a4f911611694103af05346637df1b7"}, - {file = "numpy-1.25.1-cp39-cp39-win32.whl", hash = "sha256:247d3ffdd7775bdf191f848be8d49100495114c82c2bd134e8d5d075fb386a1c"}, - {file = "numpy-1.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:1d5d3c68e443c90b38fdf8ef40e60e2538a27548b39b12b73132456847f4b631"}, - {file = "numpy-1.25.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:35a9527c977b924042170a0887de727cd84ff179e478481404c5dc66b4170009"}, - {file = "numpy-1.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d3fe3dd0506a28493d82dc3cf254be8cd0d26f4008a417385cbf1ae95b54004"}, - {file = "numpy-1.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:012097b5b0d00a11070e8f2e261128c44157a8689f7dedcf35576e525893f4fe"}, - {file = "numpy-1.25.1.tar.gz", hash = "sha256:9a3a9f3a61480cc086117b426a8bd86869c213fc4072e606f01c4e4b66eb92bf"}, + {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, + {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, + {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, + {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, + {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, + {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, + {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, + {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, + {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, + {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, + {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, + {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, ] [[package]] @@ -1455,18 +1576,18 @@ files = [ [[package]] name = "platformdirs" -version = "3.9.1" +version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, - {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pluggy" @@ -1851,6 +1972,24 @@ pytest = ">=7.0.0" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + [[package]] name = "pytest-httpx" version = "0.23.1" @@ -2060,6 +2199,26 @@ files = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +[[package]] +name = "stamina" +version = "23.1.0" +description = "Production-grade retries made easy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "stamina-23.1.0-py3-none-any.whl", hash = "sha256:850de8c2c2469aabf42a4c02e7372eaa12c2eced78f2bfa34162b8676c2846e5"}, + {file = "stamina-23.1.0.tar.gz", hash = "sha256:b16ce3d52d658aa75db813fc6a6661b770abfea915f72cda48e325f2a7854786"}, +] + +[package.dependencies] +tenacity = "*" + +[package.extras] +dev = ["nox", "prometheus-client", "stamina[tests,typing]", "structlog", "tomli"] +docs = ["furo", "myst-parser", "prometheus-client", "sphinx", "sphinx-notfound-page", "structlog"] +tests = ["pytest", "pytest-asyncio"] +typing = ["mypy (>=1.4)"] + [[package]] name = "starlette" version = "0.27.0" @@ -2108,6 +2267,20 @@ files = [ [package.dependencies] mpmath = ">=0.19" +[[package]] +name = "tenacity" +version = "8.2.2" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.6" +files = [ + {file = "tenacity-8.2.2-py3-none-any.whl", hash = "sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0"}, + {file = "tenacity-8.2.2.tar.gz", hash = "sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + [[package]] name = "tokenizers" version = "0.13.3" @@ -2164,20 +2337,20 @@ testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests"] [[package]] name = "tqdm" -version = "4.65.0" +version = "4.65.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, - {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, + {file = "tqdm-4.65.1-py3-none-any.whl", hash = "sha256:16181c62ad2c6f8f6f29876e66322faad1c7fd3cc70aa9cc25ff63e50d1da031"}, + {file = "tqdm-4.65.1.tar.gz", hash = "sha256:2cb0075cc5269f8edac40bdeb757cc36ab5b6648caf014822b67e1a49fba141d"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] @@ -2211,13 +2384,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.23.1" +version = "0.23.2" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.23.1-py3-none-any.whl", hash = "sha256:1d55d46b83ee4ce82b4e82f621f2050adb3eb7b5481c13f9af1744951cae2f1f"}, - {file = "uvicorn-0.23.1.tar.gz", hash = "sha256:da9b0c8443b2d7ee9db00a345f1eee6db7317432c9d4400f5049cc8d358383be"}, + {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, + {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, ] [package.dependencies] @@ -2489,6 +2662,20 @@ files = [ {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, ] +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + [[package]] name = "yarl" version = "1.9.2" @@ -2579,4 +2766,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "b6097887e0343a553bec5519aec6ecf345796e27d4a0f0f4abf8cd51e56a24eb" +content-hash = "75afc46634677cd9afdf2ae66b320a8eaaa36d360d0ba187e5974b90810df44f" diff --git a/server/pyproject.toml b/server/pyproject.toml index bd10f796..039e1f5a 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -22,10 +22,12 @@ uvicorn = {extras = ["standard"], version = "^0.23.1"} fastapi = "^0.100.1" sentry-sdk = {extras = ["fastapi"], version = "^1.29.2"} httpx = "^0.24.1" +fastapi-pagination = "^0.12.6" [tool.poetry.group.dev.dependencies] black = "^23.7.0" +stamina = "^23.1.0" [tool.poetry.group.client.dependencies] @@ -33,9 +35,11 @@ pyaudio = "^0.2.13" [tool.poetry.group.tests.dependencies] +pytest-cov = "^4.1.0" pytest-aiohttp = "^1.0.4" pytest-asyncio = "^0.21.1" pytest = "^7.4.0" +httpx-ws = "^0.4.1" pytest-httpx = "^0.23.1" @@ -45,3 +49,13 @@ aioboto3 = "^11.2.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.coverage.run] +source = ["reflector"] + +[tool.pytest.ini_options] +addopts = "-ra -q --disable-pytest-warnings --cov --cov-report html -v" +testpaths = ["tests"] +asyncio_mode = "auto" + + diff --git a/server/reflector/app.py b/server/reflector/app.py index 36e86d9a..f2988498 100644 --- a/server/reflector/app.py +++ b/server/reflector/app.py @@ -1,6 +1,8 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from fastapi_pagination import add_pagination from reflector.views.rtc_offer import router as rtc_offer_router +from reflector.views.transcripts import router as transcripts_router from reflector.events import subscribers_startup, subscribers_shutdown from reflector.logger import logger from reflector.settings import settings @@ -44,6 +46,8 @@ app.add_middleware( # register views app.include_router(rtc_offer_router) +app.include_router(transcripts_router, prefix="/v1") +add_pagination(app) if __name__ == "__main__": import uvicorn diff --git a/server/reflector/models.py b/server/reflector/models.py index af04ade4..d1aaaa1e 100644 --- a/server/reflector/models.py +++ b/server/reflector/models.py @@ -199,6 +199,7 @@ class TranscriptionContext: sorted_transcripts: dict data_channel: None # FIXME logger: None + status: str def __init__(self, logger): self.transcription_text = "" @@ -206,4 +207,5 @@ class TranscriptionContext: self.incremental_responses = [] self.data_channel = None self.sorted_transcripts = SortedDict() + self.status = "idle" self.logger = logger diff --git a/server/reflector/processors/base.py b/server/reflector/processors/base.py index 7d11590d..692a490b 100644 --- a/server/reflector/processors/base.py +++ b/server/reflector/processors/base.py @@ -14,6 +14,7 @@ class Processor: if callback: self.on(callback) self.uid = uuid4().hex + self.flushed = False self.logger = (custom_logger or logger).bind(processor=self.__class__.__name__) def set_pipeline(self, pipeline: "Pipeline"): @@ -65,6 +66,7 @@ class Processor: """ # logger.debug(f"{self.__class__.__name__} push") try: + self.flushed = False return await self._push(data) except Exception: self.logger.exception("Error in push") @@ -72,8 +74,12 @@ class Processor: async def flush(self): """ Flush data to this processor + Works only one time, until another push is called """ + if self.flushed: + return # logger.debug(f"{self.__class__.__name__} flush") + self.flushed = True return await self._flush() def describe(self, level=0): diff --git a/server/reflector/processors/types.py b/server/reflector/processors/types.py index 353c8aa4..bdf98b7a 100644 --- a/server/reflector/processors/types.py +++ b/server/reflector/processors/types.py @@ -1,9 +1,8 @@ -from dataclasses import dataclass +from pydantic import BaseModel from pathlib import Path -@dataclass -class AudioFile: +class AudioFile(BaseModel): path: Path sample_rate: int channels: int @@ -14,15 +13,13 @@ class AudioFile: self.path.unlink() -@dataclass -class Word: +class Word(BaseModel): text: str start: float end: float -@dataclass -class Transcript: +class Transcript(BaseModel): text: str = "" words: list[Word] = None @@ -59,8 +56,7 @@ class Transcript: return Transcript(text=self.text, words=words) -@dataclass -class TitleSummary: +class TitleSummary(BaseModel): title: str summary: str timestamp: float @@ -75,7 +71,6 @@ class TitleSummary: return f"{minutes:02d}:{seconds:02d}.{milliseconds:03d}" -@dataclass -class FinalSummary: +class FinalSummary(BaseModel): summary: str duration: float diff --git a/server/reflector/stream_client.py b/server/reflector/stream_client.py index 912bc514..b3e4d966 100644 --- a/server/reflector/stream_client.py +++ b/server/reflector/stream_client.py @@ -3,7 +3,6 @@ import time import uuid import httpx -import pyaudio import stamina from aiortc import RTCPeerConnection, RTCSessionDescription from aiortc.contrib.media import MediaPlayer, MediaRelay @@ -24,7 +23,6 @@ class StreamClient: self.server_url = url self.play_from = play_from self.ping_pong = ping_pong - self.paudio = pyaudio.PyAudio() self.pc = RTCPeerConnection() @@ -74,7 +72,7 @@ class StreamClient: async def on_connectionstatechange(): self.logger.info(f"Connection state is {pc.connectionState}") if pc.connectionState == "failed": - await pc.close() + await self.stop() self.pcs.discard(pc) @pc.on("track") @@ -87,8 +85,9 @@ class StreamClient: self.logger.info(f"Track {track.kind} ended") self.pc.addTrack(audio) + self.track_audio = audio - channel = pc.createDataChannel("data-channel") + self.channel = channel = pc.createDataChannel("data-channel") self.logger = self.logger.bind(channel=channel.label) self.logger.info("Created by local party") @@ -142,3 +141,6 @@ class StreamClient: coro = self.run_offer(self.pc, self.signaling) task = asyncio.create_task(coro) await task + + def is_ended(self): + return self.track_audio is None or self.track_audio.readyState == "ended" diff --git a/server/reflector/views/rtc_offer.py b/server/reflector/views/rtc_offer.py index 11c98009..cbc0a4dc 100644 --- a/server/reflector/views/rtc_offer.py +++ b/server/reflector/views/rtc_offer.py @@ -6,6 +6,7 @@ from reflector.models import TranscriptionContext from reflector.logger import logger from aiortc import RTCPeerConnection, RTCSessionDescription, MediaStreamTrack from json import loads, dumps +from enum import StrEnum import av from reflector.processors import ( Pipeline, @@ -51,8 +52,20 @@ class RtcOffer(BaseModel): type: str -@router.post("/offer") -async def rtc_offer(params: RtcOffer, request: Request): +class StrValue(BaseModel): + value: str + + +class PipelineEvent(StrEnum): + TRANSCRIPT = "TRANSCRIPT" + TOPIC = "TOPIC" + FINAL_SUMMARY = "FINAL_SUMMARY" + STATUS = "STATUS" + + +async def rtc_offer_base( + params: RtcOffer, request: Request, event_callback=None, event_callback_args=None +): # build an rtc session offer = RTCSessionDescription(sdp=params.sdp, type=params.type) @@ -62,14 +75,36 @@ async def rtc_offer(params: RtcOffer, request: Request): ctx = TranscriptionContext(logger=logger.bind(client=clientid)) ctx.topics = [] + async def update_status(status: str): + changed = ctx.status != status + if changed: + ctx.status = status + if event_callback: + await event_callback( + event=PipelineEvent.STATUS, + args=event_callback_args, + data=StrValue(value=status), + ) + # build pipeline callback async def on_transcript(transcript: Transcript): ctx.logger.info("Transcript", transcript=transcript) - result = { - "cmd": "SHOW_TRANSCRIPTION", - "text": transcript.text, - } - ctx.data_channel.send(dumps(result)) + + # send to RTC + if ctx.data_channel.readyState == "open": + result = { + "cmd": "SHOW_TRANSCRIPTION", + "text": transcript.text, + } + ctx.data_channel.send(dumps(result)) + + # send to callback (eg. websocket) + if event_callback: + await event_callback( + event=PipelineEvent.TRANSCRIPT, + args=event_callback_args, + data=transcript, + ) async def on_topic(summary: TitleSummary): # FIXME: make it incremental with the frontend, not send everything @@ -82,17 +117,37 @@ async def rtc_offer(params: RtcOffer, request: Request): "desc": summary.summary, } ) - result = {"cmd": "UPDATE_TOPICS", "topics": ctx.topics} - ctx.data_channel.send(dumps(result)) + + # send to RTC + if ctx.data_channel.readyState == "open": + result = {"cmd": "UPDATE_TOPICS", "topics": ctx.topics} + ctx.data_channel.send(dumps(result)) + + # send to callback (eg. websocket) + if event_callback: + await event_callback( + event=PipelineEvent.TOPIC, args=event_callback_args, data=summary + ) async def on_final_summary(summary: FinalSummary): ctx.logger.info("FinalSummary", final_summary=summary) - result = { - "cmd": "DISPLAY_FINAL_SUMMARY", - "summary": summary.summary, - "duration": summary.duration, - } - ctx.data_channel.send(dumps(result)) + + # send to RTC + if ctx.data_channel.readyState == "open": + result = { + "cmd": "DISPLAY_FINAL_SUMMARY", + "summary": summary.summary, + "duration": summary.duration, + } + ctx.data_channel.send(dumps(result)) + + # send to callback (eg. websocket) + if event_callback: + await event_callback( + event=PipelineEvent.FINAL_SUMMARY, + args=event_callback_args, + data=summary, + ) # create a context for the whole rtc transaction # add a customised logger to the context @@ -108,11 +163,13 @@ async def rtc_offer(params: RtcOffer, request: Request): # handle RTC peer connection pc = RTCPeerConnection() - async def flush_pipeline_and_quit(): - ctx.logger.info("Flushing pipeline") + async def flush_pipeline_and_quit(close=True): + await update_status("processing") await ctx.pipeline.flush() - ctx.logger.debug("Closing peer connection") - await pc.close() + if close: + ctx.logger.debug("Closing peer connection") + await pc.close() + await update_status("ended") @pc.on("datachannel") def on_datachannel(channel): @@ -135,11 +192,14 @@ async def rtc_offer(params: RtcOffer, request: Request): ctx.logger.info(f"Connection state: {pc.connectionState}") if pc.connectionState == "failed": await pc.close() + elif pc.connectionState == "closed": + await flush_pipeline_and_quit(close=False) @pc.on("track") def on_track(track): ctx.logger.info(f"Track {track.kind} received") pc.addTrack(AudioStreamTrack(ctx, track)) + asyncio.get_event_loop().create_task(update_status("recording")) await pc.setRemoteDescription(offer) @@ -157,3 +217,8 @@ async def rtc_clean_sessions(): logger.debug(f"Closing session {pc}") await pc.close() sessions.clear() + + +@router.post("/offer") +async def rtc_offer(params: RtcOffer, request: Request): + return await rtc_offer_base(params, request) diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py new file mode 100644 index 00000000..039201ec --- /dev/null +++ b/server/reflector/views/transcripts.py @@ -0,0 +1,315 @@ +from fastapi import APIRouter, HTTPException, Request, WebSocket, WebSocketDisconnect +from pydantic import BaseModel, Field +from uuid import UUID, uuid4 +from datetime import datetime +from fastapi_pagination import Page, paginate +from reflector.logger import logger +from .rtc_offer import rtc_offer_base, RtcOffer, PipelineEvent +import asyncio +from typing import Optional + + +router = APIRouter() + +# ============================================================== +# Models to move to a database, but required for the API to work +# ============================================================== + + +def generate_transcript_name(): + now = datetime.utcnow() + return f"Transcript {now.strftime('%Y-%m-%d %H:%M:%S')}" + + +class TranscriptText(BaseModel): + text: str + + +class TranscriptTopic(BaseModel): + id: UUID = Field(default_factory=uuid4) + title: str + summary: str + transcript: str + timestamp: float + + +class TranscriptFinalSummary(BaseModel): + summary: str + + +class TranscriptEvent(BaseModel): + event: str + data: dict + + +class Transcript(BaseModel): + id: UUID = Field(default_factory=uuid4) + name: str = Field(default_factory=generate_transcript_name) + status: str = "idle" + locked: bool = False + duration: float = 0 + created_at: datetime = Field(default_factory=datetime.utcnow) + summary: str | None = None + topics: list[TranscriptTopic] = [] + events: list[TranscriptEvent] = [] + + def add_event(self, event: str, data: BaseModel) -> TranscriptEvent: + ev = TranscriptEvent(event=event, data=data.model_dump()) + self.events.append(ev) + return ev + + def upsert_topic(self, topic: TranscriptTopic): + existing_topic = next((t for t in self.topics if t.id == topic.id), None) + if existing_topic: + existing_topic.update_from(topic) + else: + self.topics.append(topic) + + +class TranscriptController: + transcripts: list[Transcript] = [] + + def get_all(self) -> list[Transcript]: + return self.transcripts + + def get_by_id(self, transcript_id: UUID) -> Transcript | None: + return next((t for t in self.transcripts if t.id == transcript_id), None) + + def add(self, transcript: Transcript): + self.transcripts.append(transcript) + + def remove(self, transcript: Transcript): + self.transcripts.remove(transcript) + + +transcripts_controller = TranscriptController() + + +# ============================================================== +# Transcripts list +# ============================================================== + + +class GetTranscript(BaseModel): + id: UUID + name: str + status: str + locked: bool + duration: int + created_at: datetime + + +class CreateTranscript(BaseModel): + name: str + + +class UpdateTranscript(BaseModel): + name: Optional[str] = Field(None) + locked: Optional[bool] = Field(None) + + +class TranscriptEntryCreate(BaseModel): + name: str + + +class DeletionStatus(BaseModel): + status: str + + +@router.get("/transcripts", response_model=Page[GetTranscript]) +async def transcripts_list(): + return paginate(transcripts_controller.get_all()) + + +@router.post("/transcripts", response_model=GetTranscript) +async def transcripts_create(info: CreateTranscript): + transcript = Transcript() + transcript.name = info.name + transcripts_controller.add(transcript) + return transcript + + +# ============================================================== +# Single transcript +# ============================================================== + + +@router.get("/transcripts/{transcript_id}", response_model=GetTranscript) +async def transcript_get(transcript_id: UUID): + transcript = transcripts_controller.get_by_id(transcript_id) + if not transcript: + raise HTTPException(status_code=404, detail="Transcript not found") + return transcript + + +@router.patch("/transcripts/{transcript_id}", response_model=GetTranscript) +async def transcript_update(transcript_id: UUID, info: UpdateTranscript): + transcript = transcripts_controller.get_by_id(transcript_id) + if not transcript: + raise HTTPException(status_code=404, detail="Transcript not found") + if info.name is not None: + transcript.name = info.name + if info.locked is not None: + transcript.locked = info.locked + return transcript + + +@router.delete("/transcripts/{transcript_id}", response_model=DeletionStatus) +async def transcript_delete(transcript_id: UUID): + transcript = transcripts_controller.get_by_id(transcript_id) + if not transcript: + raise HTTPException(status_code=404, detail="Transcript not found") + transcripts_controller.remove(transcript) + return DeletionStatus(status="ok") + + +@router.get("/transcripts/{transcript_id}/audio") +async def transcript_get_audio(transcript_id: UUID): + transcript = transcripts_controller.get_by_id(transcript_id) + if not transcript: + raise HTTPException(status_code=404, detail="Transcript not found") + + # TODO: Implement audio generation + return HTTPException(status_code=500, detail="Not implemented") + + +@router.get("/transcripts/{transcript_id}/topics", response_model=list[TranscriptTopic]) +async def transcript_get_topics(transcript_id: UUID): + transcript = transcripts_controller.get_by_id(transcript_id) + if not transcript: + raise HTTPException(status_code=404, detail="Transcript not found") + return transcript.topics + + +@router.get("/transcripts/{transcript_id}/events") +async def transcript_get_websocket_events(transcript_id: UUID): + pass + + +# ============================================================== +# Websocket Manager +# ============================================================== + + +class WebsocketManager: + def __init__(self): + self.active_connections = {} + + async def connect(self, transcript_id: UUID, websocket: WebSocket): + await websocket.accept() + if transcript_id not in self.active_connections: + self.active_connections[transcript_id] = [] + self.active_connections[transcript_id].append(websocket) + + def disconnect(self, transcript_id: UUID, websocket: WebSocket): + if transcript_id not in self.active_connections: + return + self.active_connections[transcript_id].remove(websocket) + if not self.active_connections[transcript_id]: + del self.active_connections[transcript_id] + + async def send_json(self, transcript_id: UUID, message): + if transcript_id not in self.active_connections: + return + for connection in self.active_connections[transcript_id][:]: + try: + await connection.send_json(message) + except Exception: + self.active_connections[transcript_id].remove(connection) + + +ws_manager = WebsocketManager() + + +@router.websocket("/transcripts/{transcript_id}/events") +async def transcript_events_websocket(transcript_id: UUID, websocket: WebSocket): + transcript = transcripts_controller.get_by_id(transcript_id) + if not transcript: + raise HTTPException(status_code=404, detail="Transcript not found") + + await ws_manager.connect(transcript_id, websocket) + + # on first connection, send all events + for event in transcript.events: + await websocket.send_json(event.model_dump(mode="json")) + + # XXX if transcript is final (locked=True and status=ended) + # XXX send a final event to the client and close the connection + + # endless loop to wait for new events + try: + while True: + await asyncio.sleep(42) + except WebSocketDisconnect: + ws_manager.disconnect(transcript_id, websocket) + + +# ============================================================== +# Web RTC +# ============================================================== + + +async def handle_rtc_event(event: PipelineEvent, args, data): + # OFC the current implementation is not good, + # but it's just a POC before persistence. It won't query the + # transcript from the database for each event. + # print(f"Event: {event}", args, data) + transcript_id = args + transcript = transcripts_controller.get_by_id(transcript_id) + if not transcript: + return + + # event send to websocket clients may not be the same as the event + # received from the pipeline. For example, the pipeline will send + # a TRANSCRIPT event with all words, but this is not what we want + # to send to the websocket client. + + # FIXME don't do copy + if event == PipelineEvent.TRANSCRIPT: + resp = transcript.add_event(event=event, data=TranscriptText(text=data.text)) + + elif event == PipelineEvent.TOPIC: + topic = TranscriptTopic( + title=data.title, + summary=data.summary, + transcript=data.transcript.text, + timestamp=data.timestamp, + ) + resp = transcript.add_event(event=event, data=topic) + transcript.upsert_topic(topic) + + elif event == PipelineEvent.FINAL_SUMMARY: + final_summary = TranscriptFinalSummary(summary=data.summary) + resp = transcript.add_event(event=event, data=final_summary) + transcript.summary = final_summary + + elif event == PipelineEvent.STATUS: + resp = transcript.add_event(event=event, data=data) + transcript.status = data.value + + else: + logger.warning(f"Unknown event: {event}") + return + + # transmit to websocket clients + await ws_manager.send_json(transcript_id, resp.model_dump(mode="json")) + + +@router.post("/transcripts/{transcript_id}/record/webrtc") +async def transcript_record_webrtc( + transcript_id: UUID, params: RtcOffer, request: Request +): + transcript = transcripts_controller.get_by_id(transcript_id) + if not transcript: + raise HTTPException(status_code=404, detail="Transcript not found") + + if transcript.locked: + raise HTTPException(status_code=400, detail="Transcript is locked") + + # FIXME do not allow multiple recording at the same time + return await rtc_offer_base( + params, + request, + event_callback=handle_rtc_event, + event_callback_args=transcript_id, + ) diff --git a/server/tests/records/test_short.wav b/server/tests/records/test_short.wav new file mode 100644 index 00000000..ca3026c9 Binary files /dev/null and b/server/tests/records/test_short.wav differ diff --git a/server/tests/test_transcripts.py b/server/tests/test_transcripts.py new file mode 100644 index 00000000..77cb4b23 --- /dev/null +++ b/server/tests/test_transcripts.py @@ -0,0 +1,78 @@ +import pytest +from httpx import AsyncClient +from reflector.app import app + + +@pytest.mark.asyncio +async def test_transcript_create(): + async with AsyncClient(app=app, base_url="http://test/v1") as ac: + response = await ac.post("/transcripts", json={"name": "test"}) + assert response.status_code == 200 + assert response.json()["name"] == "test" + assert response.json()["status"] == "idle" + assert response.json()["locked"] is False + assert response.json()["id"] is not None + assert response.json()["created_at"] is not None + + # ensure some fields are not returned + assert "topics" not in response.json() + assert "events" not in response.json() + + +@pytest.mark.asyncio +async def test_transcript_get_update_name(): + async with AsyncClient(app=app, base_url="http://test/v1") as ac: + response = await ac.post("/transcripts", json={"name": "test"}) + assert response.status_code == 200 + assert response.json()["name"] == "test" + + tid = response.json()["id"] + + response = await ac.get(f"/transcripts/{tid}") + assert response.status_code == 200 + assert response.json()["name"] == "test" + + response = await ac.patch(f"/transcripts/{tid}", json={"name": "test2"}) + assert response.status_code == 200 + assert response.json()["name"] == "test2" + + response = await ac.get(f"/transcripts/{tid}") + assert response.status_code == 200 + assert response.json()["name"] == "test2" + + +@pytest.mark.asyncio +async def test_transcripts_list(): + # XXX this test is a bit fragile, as it depends on the storage which + # is shared between tests + async with AsyncClient(app=app, base_url="http://test/v1") as ac: + response = await ac.post("/transcripts", json={"name": "testxx1"}) + assert response.status_code == 200 + assert response.json()["name"] == "testxx1" + + response = await ac.post("/transcripts", json={"name": "testxx2"}) + assert response.status_code == 200 + assert response.json()["name"] == "testxx2" + + response = await ac.get("/transcripts") + assert response.status_code == 200 + assert len(response.json()["items"]) >= 2 + names = [t["name"] for t in response.json()["items"]] + assert "testxx1" in names + assert "testxx2" in names + + +@pytest.mark.asyncio +async def test_transcript_delete(): + async with AsyncClient(app=app, base_url="http://test/v1") as ac: + response = await ac.post("/transcripts", json={"name": "testdel1"}) + assert response.status_code == 200 + assert response.json()["name"] == "testdel1" + + tid = response.json()["id"] + response = await ac.delete(f"/transcripts/{tid}") + assert response.status_code == 200 + assert response.json()["status"] == "ok" + + response = await ac.get(f"/transcripts/{tid}") + assert response.status_code == 404 diff --git a/server/tests/test_transcripts_rtc_ws.py b/server/tests/test_transcripts_rtc_ws.py new file mode 100644 index 00000000..70ee209b --- /dev/null +++ b/server/tests/test_transcripts_rtc_ws.py @@ -0,0 +1,190 @@ +# === further tests +# FIXME test status of transcript +# FIXME test websocket connection after RTC is finished still send the full events +# FIXME try with locked session, RTC should not work + +import pytest +import json +from unittest.mock import patch +from httpx import AsyncClient + +from reflector.app import app +from uvicorn import Config, Server +import threading +import asyncio +from pathlib import Path +from httpx_ws import aconnect_ws + + +class ThreadedUvicorn: + def __init__(self, config: Config): + self.server = Server(config) + self.thread = threading.Thread(daemon=True, target=self.server.run) + + async def start(self): + self.thread.start() + while not self.server.started: + await asyncio.sleep(0.1) + + def stop(self): + if self.thread.is_alive(): + self.server.should_exit = True + while self.thread.is_alive(): + continue + + +@pytest.fixture +async def dummy_transcript(): + from reflector.processors.audio_transcript import AudioTranscriptProcessor + from reflector.processors.types import AudioFile, Transcript, Word + + class TestAudioTranscriptProcessor(AudioTranscriptProcessor): + async def _transcript(self, data: AudioFile): + return Transcript( + text="Hello world", + words=[ + Word(start=0.0, end=1.0, text="Hello"), + Word(start=1.0, end=2.0, text="world"), + ], + ) + + with patch( + "reflector.processors.audio_transcript_auto" + ".AudioTranscriptAutoProcessor.get_instance" + ) as mock_audio: + mock_audio.return_value = TestAudioTranscriptProcessor() + yield + + +@pytest.fixture +async def dummy_llm(): + from reflector.llm.base import LLM + + class TestLLM(LLM): + async def _generate(self, prompt: str, **kwargs): + return json.dumps({"title": "LLM TITLE", "summary": "LLM SUMMARY"}) + + with patch("reflector.llm.base.LLM.get_instance") as mock_llm: + mock_llm.return_value = TestLLM() + yield + + +@pytest.mark.asyncio +async def test_transcript_rtc_and_websocket(dummy_transcript, dummy_llm): + # goal: start the server, exchange RTC, receive websocket events + # because of that, we need to start the server in a thread + # to be able to connect with aiortc + + # start server + host = "127.0.0.1" + port = 1255 + base_url = f"http://{host}:{port}/v1" + config = Config(app=app, host=host, port=port) + server = ThreadedUvicorn(config) + await server.start() + + # create a transcript + ac = AsyncClient(base_url=base_url) + response = await ac.post("/transcripts", json={"name": "Test RTC"}) + assert response.status_code == 200 + tid = response.json()["id"] + + # create a websocket connection as a task + events = [] + + async def websocket_task(): + print("Test websocket: TASK STARTED") + async with aconnect_ws(f"{base_url}/transcripts/{tid}/events") as ws: + print("Test websocket: CONNECTED") + try: + while True: + msg = await ws.receive_json() + print(f"Test websocket: JSON {msg}") + if msg is None: + break + events.append(msg) + except Exception as e: + print(f"Test websocket: EXCEPTION {e}") + finally: + ws.close() + print("Test websocket: DISCONNECTED") + + websocket_task = asyncio.get_event_loop().create_task(websocket_task()) + + # create stream client + import argparse + from reflector.stream_client import StreamClient + from aiortc.contrib.signaling import add_signaling_arguments, create_signaling + + parser = argparse.ArgumentParser() + add_signaling_arguments(parser) + args = parser.parse_args(["-s", "tcp-socket"]) + signaling = create_signaling(args) + + url = f"{base_url}/transcripts/{tid}/record/webrtc" + path = Path(__file__).parent / "records" / "test_short.wav" + client = StreamClient(signaling, url=url, play_from=path.as_posix()) + await client.start() + + timeout = 20 + while not client.is_ended(): + await asyncio.sleep(1) + timeout -= 1 + if timeout < 0: + raise TimeoutError("Timeout while waiting for RTC to end") + + # XXX aiortc is long to close the connection + # instead of waiting a long time, we just send a STOP + client.channel.send(json.dumps({"cmd": "STOP"})) + + # wait the processing to finish + await asyncio.sleep(2) + + await client.stop() + + # wait the processing to finish + await asyncio.sleep(2) + + # stop websocket task + websocket_task.cancel() + + # check events + assert len(events) > 0 + from pprint import pprint + + pprint(events) + + # get events list + eventnames = [e["event"] for e in events] + + # check events + assert "TRANSCRIPT" in eventnames + ev = events[eventnames.index("TRANSCRIPT")] + assert ev["data"]["text"] == "Hello world" + + assert "TOPIC" in eventnames + ev = events[eventnames.index("TOPIC")] + assert ev["data"]["id"] + assert ev["data"]["summary"] == "LLM SUMMARY" + assert ev["data"]["transcript"].startswith("Hello world") + assert ev["data"]["timestamp"] == 0.0 + + assert "FINAL_SUMMARY" in eventnames + ev = events[eventnames.index("FINAL_SUMMARY")] + assert ev["data"]["summary"] == "LLM SUMMARY" + + # check status order + statuses = [e["data"]["value"] for e in events if e["event"] == "STATUS"] + assert statuses == ["recording", "processing", "ended"] + + # ensure the last event received is ended + assert events[-1]["event"] == "STATUS" + assert events[-1]["data"]["value"] == "ended" + + # stop server + # server.stop() + + # check that transcript status in model is updated + resp = await ac.get(f"/transcripts/{tid}") + assert resp.status_code == 200 + assert resp.json()["status"] == "ended" diff --git a/www/app/apple-icon.png b/www/app/apple-icon.png deleted file mode 100644 index 9cc49755..00000000 Binary files a/www/app/apple-icon.png and /dev/null differ diff --git a/www/app/components/dashboard.js b/www/app/components/dashboard.js index 9c73f6f3..becbd4a5 100644 --- a/www/app/components/dashboard.js +++ b/www/app/components/dashboard.js @@ -36,6 +36,18 @@ export function Dashboard({ } }; + const formatTime = (seconds) => { + let hours = Math.floor(seconds / 3600); + let minutes = Math.floor((seconds % 3600) / 60); + let secs = Math.floor(seconds % 60); + + let timeString = `${hours > 0 ? hours + ":" : ""}${minutes + .toString() + .padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; + + return timeString; + }; + return ( <>
Duration: {finalSummary.duration}
{finalSummary.summary}
Capture The Signal, Not The Noise