diff --git a/audio/Received-from-BOT.txt b/audio/Received-from-BOT.txt new file mode 100644 index 0000000000000000000000000000000000000000..db9c0d5f4647e799ba4fcd1fdafb387f1780c1b0 --- /dev/null +++ b/audio/Received-from-BOT.txt @@ -0,0 +1,92 @@ +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAAAAAAAAAAAAAAAAAAAA//8AAAAAAAD//////////////////wAA/////wAAAAD/////AAD//wAAAAABAP//AAAAAAEAAAAAAAEAAQABAAAAAQACAAEAAAABAAMAAAD//wEAAQABAAEAAAABAAAAAgD+/wAA//8BAPr/7f/z/+r/7P/w/+H/1//a/+L/5f/j/+r/8/////b/5v/j//3/AgDt/9r/6f8DAAAA7//9/xYADQDs/+//BgAJAAUAAgAJAA4ADwARABgAIAAlADIAMwAxAEQAUQBZAFIASgA/ADgAOwBFAEUARwBRAFMAOwAvAEIATQBPAEMAQgBJAEQAOgA8AEEAPQA9ADUAHwAgACYALQAaABIAFQATAAQA9f8FAPP/7v/w/+v/8f/X/83/x//L/83/yf/Q/7n/v/+9/8P/wP++/8j/yf/B/67/uv/X/9T/0v/I/9n/3v/K/9D/0v/0//P/8f/O/9z/8f/4//r/7v8LAAQAAwANAAwADgAaABkACwAZACMAMAA3ACMAHAAnACkAKwAoADoAPQA5AD0ANQAuACMARgBEADAANgBAAD8AKwApADIAOwAnAAsAEAASABEAGwAWAA4A/f/6//3/AQD6/+7/9v/+//X/4f/k/+r/5//s//X/7P/a/9j/4P/v/+v/2v/k/+X/5P/i/9T/2v/r//3/+P/7/wAA+f8IAAIABgAKACIAFAD9/xIAGwAsACMANgArAC4AJQAYACEAMwBTAEMAIgApADQAMAAWAC0ARgBFACAAGAAnACUAHwAPACAAIwAVAAkABQDy//T/FQAVAO7/5//1//D/3f/d//j//P/a/8L/wv+9/8X/2f/h/87/s//A/7v/u//C/73/vv+8/7//vP+//8T/y//I/8D/vf/K/9P/1f/P/9b/2//T/9f/2v/f/+r/4v/X/9f/7f/6//D/7//q//P/8P/1//7/AgALAA0ACgAHAAkAEAAQAA4ADgAZABMABgAJABYAGAAGAAwAEwAXABAABwAHAAoAEAAVABIACAABAAIABgAGABAAEAAWAA0A/P/4/wQACAADAAcABwAAAPP/8f/+/woABgALAAAA6//q//3/BAD1/wEABAD3//b/AwAKAPz/+P8JABoAEAAOAB0AGAAOAAcADAAaACYALQAoABgAHAAoACgAJwAiABgALQAwAB8AEQAkADgAMQAbAAcAIgAmACEABQAVACcAGwAIAPT/BgAXABwA/P/x/+//9f/x//n////0/+r/3f/b/+D/6v/u/+n/3P/U/8b/yP/T/93/4f/g/9f/vv+7/8j/2//c/9b/z//C/7j/wP/W/9//5v/d/83/u//G/9H/3f/Z/97/3//T/9H/2//1/+X/3P/j//r/9v/k/+r////+/+n/8P8AAAQA9f/1//v/BAD///v/AwD7/+z/9f8DAAAA+v/3//H/6v/t//P/9//y//f/9P/k/9j/4f/2/+7/6f/f/+L/6f/w/+f/3v/m/+P/5f/g/+f/7f/1/+X/4v/s//z/+//x//r/BgAIAPz/AAACAAoAHgAhABEADAAdACEAGwAdACkAMwAjABQAFwAmADQAOgBBADgAMgAwADQAOwBAAEQARABAADkAOwBDAEYAQABDAEIAPQAzADIANQA5ADMAMAAsACQAIAAhABoAFAAXAB0AEgAIAAkADQANAAcACQAOABAA+f/p/+f/+/8EAAAA/f/0//D/4f/m//T/9P/j/9z/3//Z/9b/3//u/w=="} (15:47:59.625) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "5f/Y/9P/2P/d/+P/6v/h/93/1v/c/+T/6P/y//X/8f/g/+f/+P/9//X/8P/5//j/8v/y/wEADQAHAAUA/f/2//T/AQAKAAYAAgD+//3/AQAGAAEA9v/3/wIAAgD6//b/+P/4/+3/6f/o/+7/7//r/+X/2f/e/+T/5P/e/9f/1f/S/9D/zf/W/9z/0f/O/8v/0v/U/9j/2v/S/9f/2//d/9f/2P/g/+n/5f/g/+b/8//x/+r/8v8CAAsABgADAAgAEAAWAB8AIQAdABwAIAAnADIANwA5ADcANAAsADIAOgA8ADkAOgA8ADMAKQAmADYAPwA6AC0AIAAhACEAKwApACgAJQAaABQAGwAlABwADQAHABAAEQALAAoADQAIAPn/8//1//3/+f/y/+7/8P/1/+//5v/k/+z/7//o/+T/5v/v//T/9P/0//P/8P/r//H/9P/9/woAEQAKAPv/+f8FABcAGQASAA0AEAASABMAHAAmACUAGAAMAA0AGQAiACMAHAAUAA8ADAAMAA8AFAATAAwAAwD9/wEABAAEAP3/+P/1//D/7f/u//T/9P/r/+H/3f/g/+D/3//e/93/2f/S/9D/0//X/9P/yv/H/8r/0P/R/9D/0v/Q/83/zP/R/9j/2f/Y/9b/2v/e/+H/4v/j/+H/4f/m/+r/7v/w//L/8//0//X/+f8AAAIAAgABAAMABwAKAA0ADwARABIAEgAUABcAGgAZABgAGAAZABsAGgAbABwAHAAbABkAGQAXABQAEwAUABMAEQAOAAoACQAJAAcABAADAAIA///9//v//P/8//f/8//x//P/9f/1//T/8v/y//D/8P/y//P/9P/z//L/8//1//b/9v/2//f/+f/4//j/+v/9//7//v8BAAQABwAKAAsADAAMAA0ADgAPABEAEgATABMAEwAVABYAFwAWABYAFgAZABkAGgAYABcAFgAWABYAFAASAA8AEAANAAsACQAIAAcABQACAAAA///9//v/+P/1//P/8f/w/+//7f/r/+r/6//q/+r/6f/n/+b/6P/p/+n/6P/p/+z/7v/w//D/8f/z//X/+f/7//z//v8BAAMABgAIAAoADAAOABEAEwAUABYAGAAXABcAFgAYABoAGQAZABgAGAAYABcAFAATABEAEQAPAA0ACwAJAAgABgADAAAAAAD///3/+//4//f/9f/0//T/8f/w//D/7v/u/+z/6//q/+j/5//n/+b/5f/k/+T/4//k/+T/5P/k/+T/5f/m/+b/6P/p/+r/6v/s/+3/7//x//P/9P/1//f/+f/7//z//v8AAAIABQAGAAcACAAKAAsADQAOAA4ADwAQABIAEQATABQAEgASABIAEwASAA4ADQAMAAsACgAJAAkABwAGAAYABgAFAAUABQAEAAQAAwADAAIAAgABAAEAAAD+//3//f/9//z//f/+//7//v/8//3//f/+//3///8AAP7//v///wAA/////wEAAgACAAQABAAEAAUABgAGAAcACgAKAAwADgALAAoACwANAA0ADAAKAA0ACQAJAAsACQAHAAUABwAKAAUABgAKAAUABwD6//f/+P8jAB4A5P/X/8f/5v8XACcA///u//T/CAAKAMb/3f9AAE8A7f9h/47/HgBcADEAuv9u/5j/GABAAN7/rf/2/yIA9P/F/8//6v/Z//f/RgA+APX/w/8yABgAk/+b/ygApAA0AG7/LP8TANQAFAAJ/+H+7/8HAaEAev9x/zsABwCB/4H////UAJcA0f+F/6r/u/9l/7T/1QBtAQEAJ/6p/rMAVAFcAU8Aa/8B/wb//v/oAIUBCQAL/5L/awAIAFb/k/9oAMUBZwD0/ir/VADsAdEB0/zn/BMBjAJfAOb+HAB8AKr/uf8tAEAA0f/Q/iQAtAAfAej/2P4w/13/cAGEAKz/jv7l/igBqABPAOgArP82ABj/kv/z/zgACQFgAOcAwv6U/WAAfgB3A1QBg/8l/qr+7ADf/9z+3f6RAc0CMAF+/D793wB+A40Co/+6/bP8f/97ABEDSgRQAnv/dPj1+Nr9fAZtB2cBtf2r/Lj9DwBZALEA7f49/3cB2AETAJj9Uf/rA0H/3v6HAB//lACt/9YDTP7m/w=="} (15:47:59.648) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.666) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.686) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "bCjAJsMZqhD/B0n+avmX+p37IwBvBjoKzg1cEyMSUBJxESgGwQBW9uPppOFR3L3VENG119PWGN5Z6PX0ZgcvEW4iASYZJ8IogB45F64NVAXP/DD7sfuo+5QBOAbSBn8PcxHKEFoUSgtOBqb8Le/r5i/dR9Xv0HjSwtK/2Szib/BcA44NXSEJJ3sptS1NJL0drRMyC4oAnvq4/Jb1Kvzn/3gAigrGDIoSQhPlDw0Lyf9R9dTp8t5L1UHRMM/vzjbYmNwL7n4BNAtEIN4n0isyMMgp6iFzGG8OnQNt/J75rfVW9sH6Ef3/BIsKLRCWFBMRdA6BBEz44+1b4G/Wp9DTzO/NvtQF25vtMP+UC1gfRSeoLCAwmitJJDUadxHNBhT+cPon9iX19fcG+2gBswe0DQsR+A8PDiEFxfop8NDi69iY0kDOCc7V0x/bIuuL+o0JaxrIJMQthS1KLpApdx1rGN0L9AK7/A312vRh9Ov2pfw7AocIiwsyDYoLrgXG/Erzp+j23GzWcNLa0NvUrNzj68r5CQmBGLkj5CteLPcusin6H10Z4A2+BWP8WvZG9GPyP/Wr+XP/XwXSCEwL1gnaBET9YvM16ZPesNgV1R7T0dZh34vtA/uqCH0WHiL8KKsqaCz2KP8hAxmND3YH4P2K99bzNvLl8+b32f3TAhsG1wiGBz4D7/sy85LqN+Et3MnYOtcz2vjiMfB2/OYH/BMHH7slNigoKXgn/CKuGk0RxAhjAPf54PRY8lrz/vbv+xMATwOoBVYFQAL9+7n0s+2A5jHhadyX2pTcPuMR7gX5bgN9DRIXBh8tJMYmUyaAI8YdUhZPDt4F4v4A+SP1cvTC9ej46/tZ/t8AQwHn//L7pvaB8S/sB+jG42vh1eHy5ZntQ/bh/t8Gdg6tFS4cbCACIu8hzB6kGH4RiApcBAD/4vp++LT3yfiW+kL8nP1u/tP9bPs2+CL1dfKc70TsIulo507otezo8on5Rf8eA6MGygocELwVyxkwGzQZXRVCEaUNHgrnBaQBTP7P/Fj9KP5v/on94vt3+kH5gfjb9872/vQ+8l7vVe2x7ELuu/Et9nH6kv3i/2ACGAZEC6IQrhQJFpEUmxFWDpALZAl6B6MFwgNQAkQBhQDM//j+PP4u/RD8E/vA+Uf4m/bR9AXzTvE18NHvb/Ay8q70ovd5+gv9uv/cAp0GhgrQDcEPCRD9DkgNowtYCpIJKwmECJUHcAZLBTwELAP/AX0AxP72/BL7OfmD9+z1ffRA82Py3fHV8VTyT/PR9Mb2BPls+/D9lQBqA0cGAglIC+sM0w0bDu4NYQ2XDIgLQgrYCFYHyAUyBKQCHwG4/2T+FP3D+3b6PvkP+Oz25fX89EP0xfOR87fzNfQf9X72S/iQ+jb9GwAUA+8FfwibCh4M6gwTDc4MSwx0C2gKSQkACKUGUQUDBLYCbAEqAOP+lf1F/PX6rfmB+HL3ifbW9VT1C/UA9Tf1xfWz9hD4zfnW+wP+QwCDAqQEjgYqCHUJagoGC1ULVQsCC2IKdAlICOwGbgXnA2MC9gCt/3j+UP0n/Pr61fnQ+Pn3Wvf79s72y/bq9jD3pfdZ+FP5j/oD/Jn9Sv8JAc8CggQPBnAHlAhpCfAJIQr5CYUJ0QjlB9MGqgVpBBkDvwFtAC7//f3d/Mb7vvrU+RT5f/gQ+M/3tvfG9wP4cvgQ+ef59fot/ID94v5VANUBVQO/BAgGJwcVCMQILQlUCTIJ0Ag2CFwHTwYaBcEDTgLUAG3/HP7o/Mn7vPrT+Rr5i/go+PP35vcE+Ej4t/hM+Q36+PoG/Cr9XP6a/+kARwKnA/oEMgZBBxcIpgjgCNEIfAjpByYHMQYeBfYDvwJ9ATwABP/S/bT8pfux+uT5O/m7+GL4N/g9+Hv45fiN+Wj6TPtP/Gj9h/61/+cAGgJNA2wEbgVSBg0HkAfhB/YH0wd8B/AGMwZOBUcEKQP9Ab8AgP9H/gn90fuv+qT5uPj492r3Cvfs9hn3kvde+GL5lPrV+yb9iv76/20B0gIvBHgFogajB2kIBAlbCWsJLwmoCPMHBgf4BdAEhgMmAqsAJP+U/SH8xfqe+Y34lPfQ9jz2APbS9eH1G/aR9pD3yPjT+kz9MgCEAg=="} (15:47:59.707) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.726) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.747) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.768) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.788) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.808) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.829) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.850) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.871) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.892) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "hgWNAvf90vjm833w3u6d7YXsoesG6wvrN+sH65Prs+u88X7/uw6PG58ethpFFmkSvhDHDAYFiP6v+s77zP97Ap4FyAjnDOIQYhG8DvQJ+QRHAdX8wveN8vfut+6x7m/udu0n7Hjri+mh5w7mF+dr6DLwgwMeFwQorirdIVIYHA9UCeoCnPkx9Qb2dPz2BFsI7Qz4ECsVrhdzEkYLeQSe/7b+bvrY9t/0i/RF9+j1IfNH8G3sJutF58DiPOGI4I/kJ+gN9uYPbSZCNNEt1Bw6DvQCef02+EnzkvY6/xsL5BJKFCwVnBUpFZYQmQbF/jr86/3jAXMARP77/Mf8m/tk9nDwZuyJ6k/p6OVd4KHcz91V45TqyPIxBwcgXjLPM5EiCA+wAP33a/SC8b/2gQPWDwkY+hfqFKkUxRECDIkCzPvJ/QwDPwcZBh4CeQDX/i39g/i88mzwPO537L3nuuE94FngQeQi6O3rffBB/uEWACp0MCAkGBBUA/L4GPWh88H2MwS+EE8ZNRtxFmQU7A9WCGYBDvsm/mQE5wg9C74GcwRDANf6CffD8Izw+vCI7vnrzeXD4srh1eDI4nDkcen/70D+dRcOK4kxxCWdEdUCkPiN847zJPgeBlsUHRy7HPUWJxMXDocGt//B+zsANgcOCysLVgeLBIAAK/v89vHzHPWD9dDx0Oza5/3kPuKS4FrgyuNB6XjsvvANA4UecTFqMHYdCgm5/PP0sfE29GH/Ng91Gn8doxkKFIUPwgjgAJT86//bBrQK3QoGCusIBQaz/TD2hvKf9Cr4ZfZH8dTrTemF59DiHOAs37DjlumE7IXviPoHFrMuszMVJeAM1P3/9Trw6PAR+h8MBRwYIDkb9xNpD0gLgAJY/IL8KQVODcEMYwkGB2YHzwKv+O7yfPSw+u77h/U+7tzpRego5urh1uNQ6MnrX+zh6STsUPn6FRQt0TF4I+YMZ/2o9J3vX/EX/eoOtR3QIC4bFxTLDdsFKfsS9un6qwZED9QP5QyxCvAHsQBt9i/wQfJI+rr+Z/sG8wTs2eiy5KLiO+RV6hbvAO6V6qHpOe02A3ciNzN1L5sYagS3+WXyou/a8zYEZBf6IJQeuxVWEJIKYQC299P1YQDoDAwQnw6+C7YJCQVD+qb0s/Q8+Vb9Hvv49wHz5us95lPj8OTI6WLu+e2/7Izqy+pw7Pb6ChueL+QwZh1BB0P9IPZx8Lnx/v9+FMYf2BzaE4EPBQuCAQT40vXF/9MKDw5UDe4LkgqTBnT9Qvd49ij6qvzb+uv5efcq8mDqYOPA44joS+2N73HuhO4w7EPq+Oq0AH4f1C6eLCMZxghE/4b0k+6T8+4EcxaPHGYZQRTqEaQMrgGa+a/7QwQUDLkLXguuDG4JLwF799H3HfuJ+vv32/bW+Ir1ee7+5/3m4eou6y7rHO6V8H7vCOgF5xHwygVeHp0ovSc9G/oL9QBw9sTz6PkxBmgSMhcgFxgWoxHPCikAYPxd/+sDgwhBChwPpBAXCsr/Ovjj+Nr4ivf49W33gfpz9u3uFOkJ57jqqes/7X3wFfAo8Dzsuev27vL6OhPbIiknXh+VEXsJCv4t9Vn29P7DDKQSrBSWFxoWJxI3Bzr/3QDWAs8FqgaiCxIR8QpnAgP8xfoS/Nn19fNi9nn4/vW57fHqpuvp64fqWerg7tTxyu/K7ZnthPB297EGmBfIH/wekhjREFUHXvy1+Ar+7gYCDA0PzBKMFWMTqQwgBngFOAXIA2oEKAgyCrEHxATYAQUBGP65+K33EvcT9YnxIO316wrrbumI6JrqN+9e8WryJvAZ7tPvX/Lb+sgI7xLbGXsaWRgrE5IIyACh/wcE/gfXCKYM+xBrE8cRZwzCC/ELbwmRB5kFEgVEBGEDfQG//VL83Pvg+Xn3GPTl8ZfuUesS6vXoyuis6cLsfO7O7yvxIvLo8eLxd/R5+yEF8gzwEoUUaxUyEyAOBwpxBz4HqgdECQANhRBfER4RUhD9D3UOlguZCPgGDgcqBJEAYf/z/SP92vuV+ZP2GvQ78cfsU+vj6gLs+uor6gvtOe4H7v/uH/KN9IL1PvcV+p8CjggODXUQeBRxF4sUcBE7EL0PFg7eC80MXw4ZD9sN8A1PDw=="} (15:47:59.912) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.932) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.953) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:47:59.973) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "zfYZ+BH5l/o0/HH+BAFfA38GqQiRC9kNqw8kEqASMBSnE+gS1REwD3kNQgrGB9wEBQLV/zj93/s5+jL5kvjI95r39/bC9mP25vXY9Tf1F/V29B706PNG88XzbPMj9OT0VPVL9yr4OvsI/l4A/wTVBkgL+A1xEAoURRS2FrgVwRRkE60P1w2ECQ0HzwP4AH3/AP2l/G/7Efvm+lH6e/oX+ir63vnY+Y/5TflP+eT4+vij+LH4nPhU+IH45/cl+M33x/f49/z3vfhE+W36b/sB/X/+qgC1AukEXgcBCVELswxFDq4PExCvEHIQaA+eDq0M3wr8CIwGhgRWAtL/SP4C/H36Dvlt93P2RfVl9K7zFvOl8oHyY/J/8uvyU/Nu9HH1q/aH+MX5DfwX/ssAPQMqBrUIPwvnDWwP6hE2ElcTyxJvET8QQw0xC/8HWwWsAicAm/7L/Db8jfsY+0/7ufr/+rD6Jvog+h/5ofjq9932kfal9Yn1P/U19YX1t/Vc9g33wPfH+K756Pr2+2z9iP4oAOYBygPHBeEH3QnYC6AN8Q46EK0QyRBWEPcOuQ11C1UJHgeLBJECcwDJ/qD9kfzr+4P76fq4+hH6mvn9+CT4r/eu9hL2YfWb9FT0zfMB9BT0ffSs9S32IPgx+QX7LP2t/vIBcgODBgAJmwoFDoAOGBGfEcYRWRJMEKgPJw3KCoUIsAVFAyoBIP+Q/dD8p/u4+177Gvt3+8L6B/tf+tv5dPlX+ND3wfbd9Sr1UvQc9NfzKfS+9G71APfZ95P5LPt+/N7+eAAsAzYFlgcGCrgLJw4FD7YQEBH5EP4QGw9JDvIL0gnmBzQFfwNhAcH/t/6A/S39iPxg/P77pvtT+4v6KPoY+Vz4ifdk9tH15/Rb9Bf04fMe9KL0VvWF9rb3BfnG+iT8QP4MAHICoATbBkQJCQtcDYQOHBCqEKQQqhADDwAO8AuNCeYHAAVmA1gBsf/D/qb9YP3G/Lb8Mvzm+2T7nPoP+vz4O/hB9zf2nvWt9Dj0APS68zf0pPSG9fD2APjs+Zj7WP3D/5UBXAQ0Bs0IgQp3DBUO0Q4iEJoPvA+9DhQNAQx9Cf8H7QX/A5UC7gDt/+X+Qf6s/TX9nfwE/E77cfqI+ZL4qfeK9vz1xPSB9OTzgvMF9MHz6/R29cv2VPh9+b37Af1r/yoBsAO1BeAHPgqiCwgOxA4sEJQQPhAOEFMOLQ0FCx8JGQf8BFQDdgFlAAT/dv7Q/SH9+/zi+4L7cPpC+V/4CvcA9jH1VvSy87bzTfPs81/09PRb9j730vg7+sf7dv05/xsBHANMBQAHVAnCCokM+A2DDssPWA9fD68OCg1FDPUJegitBo4EKgNlASoAAP8B/gz9Nvw8+yr6Pfnn9+v24PXK9Dj0a/MW8//y9vKA8wD08/QO9i33+Pge+u77r/0H/08B2AIGBdYGnQiCCtYLfQ1RDioPkA9ADwoP2g22DEoLdgnlB/IFNwR8AuwAhP8p/gP9pPuY+jr5Hvjv9sf17fQM9IrzK/Me8zTzwfNH9Db1U/Zy9/f4ZfrO+3L99v5sADACqAOJBQ4HsQhmCo8LJA3yDacORw/eDsYO1Q2ADG8LgwnkBwQGBAQYAkIAfv64/DT7kPkS+K/2YfVB9HbzuPJm8kHyU/K98j/zCfQb9Tj2nvco+bz6RPwB/pT/UQEQA8IEtAYbCCUKYwvpDD4OyA7SD6cPpQ//DuINrwzWCj0J/wYBBbMCZwB//lP8lfqv+BT3vvVz9JXzxvJF8vPx6vEo8p/yPfNJ9GD1s/Z2+Mj5yvt6/Rv/JgGjApgEJwbiB1AJwgoJDPAMAQ5RDrcOkQ75DSgNuwteCm4InQaMBF4CcgBb/o78yvo3+cP3i/aO9Z70JPSt84HzuPP384r0VvUx9mD3yPga+tf7Wv0H/7sAJQLpAx4FjgbZB7EI4AlmCv8KhgthC4oLDgtSCp0JQQgrB68FHwSiAvcAdf/o/ZX8HPv5+db4w/cs91r2DvbU9cf1HfZn9h33s/ed+LX5pfoe/Db9hP7y//oAWQJkA20EYQVCBg0HhQdCCFkIrAjRCGsIZgizBx0HYwZjBYQEVwNlAhUBAgDx/qz93vzB++n6NvqA+Rb5uPit+A=="} (15:47:59.994) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.015) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "QQA9ADkANQAxAC0AKAAkAB8AGgAWABEADAAHAAIA/f/5//X/8f/r/+b/4f/d/9n/1P/P/8v/yP/F/8L/v/++/7z/vP+7/7z/vf+//8H/w//F/8j/yv/P/9L/1//b/+H/5f/r//L/+P/+/wUADAASABgAHQAjACkALgAyADUAOAA7AD0APgA/AD8AQAA/AD4APAA6ADYAMwAvACoAJgAhABwAFgAPAAkABAD///n/9P/u/+j/5P/e/9n/0//O/8n/xv/C/77/uv+3/7X/s/+x/6//r/+v/7D/sf+y/7P/tf+4/7z/v//C/8X/yf/N/9H/1v/b/+D/5f/r//H/9//9/wMACgAQABcAHAAiACcALQAxADUAOgA9AEAAQwBGAEYARwBIAEcARgBFAEMAQQA9ADgANQAxAC4AKgAmACEAHAAYABMADwAJAAQAAAD7//f/8v/u/+n/5f/h/97/2//Y/9X/0f/P/8z/yf/G/8X/w//C/8D/vv+9/77/vv+//8H/wv/D/8X/xv/I/8r/zP/P/9D/1P/X/9z/3//j/+j/7P/x//b/+/8AAAUACgAPABQAGQAeACMAJwAqADAAMwA3ADoAOwA/AEEAQwBEAEYARgBGAEcARABEAEIAPwA9ADsAOQA1ADEALAApACUAIQAdABcAFAAOAAoABQD///r/9v/w/+3/6v/m/+P/4f/e/9v/2v/X/9b/0//R/9H/0f/S/9H/0//T/9X/2P/a/97/4//m/+j/8P/y//j/+/8DAAcADgASABcAHwAhACoALAAxADYAOAA+AD8AQwBEAEYASABFAEgARABCAD8APAA6ADYAMgAsACoAJAAgABwAGAAVABAADQAJAAYAAQD9//r/9f/z/+7/6//n/+X/4v/f/93/2f/Z/9j/1//W/9T/1P/S/9T/0//U/9T/1v/X/9n/2v/b/93/3//h/+T/5v/p/+3/8P/2//f/+f/+/wEABAAHAAoADgAQABQAFgAZABwAHgAfACEAIQAjACIAIQAhACAAHQAdABoAGAAXABQAEgAPAAwACQAIAAQAAQD+//z/+f/2//T/8f/v/+3/6//p/+j/5f/k/+L/4P/f/93/3P/c/9v/2//a/9r/2f/b/9z/3P/d/97/4P/h/+P/5f/n/+n/7P/v//D/8//1//n//P/+/wEABQAGAAoACwAOAA8AEQASABMAFQAWABcAFwAXABcAFgAXABYAFgAUABUAFAAUABIAEgAPAA4ADgAKAAkABwAGAAQAAQD///3/+//5//f/9P/z//D/7f/t/+r/6P/o/+X/5P/h/+H/4P/f/93/3P/c/9z/2//a/9v/2v/c/9z/3P/d/93/3//f/+L/4//l/+b/6f/q/+z/7//x//T/9v/5//3/AAACAAYACAAKAA8ADgASABIAEwAUABMAFQASABQAEwATABIAFAATABIAEQARABIADwAQABAAFAATABIADQAKAAQA//8AAAAA/P/5//j/9f/0//D/7f/r/+j/5P/i/9//2//b/9j/2P/Z/9r/3f/f/+P/5f/q/+7/8f/1//r/AAAGAAoAEQAUABsAHQAkACcALAAvADQAMgA3ADMANwA2ADQAOAAxADkALwA3ACoALgApACUAJAAYABgACgAOAP3/AgD2//H/+//o//b/6v/o/+z/4v/n/93/6P/d//D/5v/w//n/+v8PAAEAEQAGAAcACAADAAIA+f/8//P/8P/u/+D/7//X/+n/1v/Y/9z/yP/P/77/v/+v/6X/mf+G/3//d/9v/3L/av9z/3r/e/+L/5H/nv+4/9n/5/8dADQAVwCTAKcA4gAOAUoBdAGzAcIB8wHwAeoB8QGxAZoBSwEAAYkADgB+/9r+Pf6C/eX8D/yF+6r6T/q1+WX5UfkE+Y75gvmY+hn7c/zu/X7/jgF2A9kF9AdwCgQMLg4ED84Pgg+4DqANsgt7CusHJga+AiMAtP2T+rb4e/UU9LrxOfD97oHtjO0Z7BHtAe3T7dPvBPH69HP3OvyNAHAFSAvtD0MVKxnLHIAeSh9aHsgbDRlYFCIQrwvfBlAD7v/W/Br74Pg09+z1q/M88g3wMu4g7EjqvOjd5rXmQ+Wu5srmWumQ6xrwbPXS+MgBqgRLDA=="} (15:48:00.035) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.056) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "XwEnAiICmAHYAA4AOP9p/sT9of1B/nb/3wDuAQ0CaQE8ACr/hf5r/tf+X//8/20AxQDkALgAPgCQ//3+0f4f/+n/4gCRAZwB9gDm/+f+Yv6S/l7/VQASAV4BPwHWAGEAAQC5/4P/UP9N/3v/4P9aAMkAGgEbAeoAkwAnAN3/vf/H/9z/DQBMAIoApwCkAH4APwDv/6j/ff90/5j/2/8wAG0AdgBHAOH/bP8N/+L++P4y/3n/qP+6/7L/k/9x/07/Pf80/zf/Rv9U/27/ef+G/4b/hP+I/5j/q//D/8r/wf+n/4b/e/+H/7n/8f8kADwAMAASAO//1v/V/+T/CAAuAFAAaQB6AIAAeABqAFIAQQA3AEUAXgB+AJ8ArgCwAJ4AhwBpAFMARwBOAF4AdgCBAIUAcABXADEAEwD+//H/7P/x//n/BwAMAAoA+P/c/7v/ov+Y/53/rv+9/8X/wv+4/6z/rv+t/7r/vf/D/8H/u/++/8H/z//g//b/DAAaACQAIgAYABIADQAYAC0ATwBuAIsAmgCZAIUAagBNAD0APwBUAHYAlQCqAK4AnQCGAGkAUwBIAEYAUgBdAGkAbABoAFgAQgArABUACAACAAMABgAFAAEA9f/m/9r/zP/A/7T/r/+q/6f/q/+o/6T/m/+J/3z/d/91/3T/e/+F/47/k/+T/4//iv+G/4X/hv+N/5j/pP+v/7f/vP++/73/uf+6/7z/yP/Y/+7/AQAQABsAHQAgACAAJQAqADIAOgBCAEoAUABSAFEATABGAEMAQABDAEUARwBFAD4ANwAvACcAIgAeABsAFgATAA4ABwD+//T/7P/l/9//2//a/9j/1v/Q/8n/w/+8/7n/uP+5/7v/vP+7/7v/uf+5/7b/uP+6/7//xf/K/9D/0v/T/9X/1v/a/97/5f/r//P//P8DAAsAEAATABUAGgAeACMAKQAvADQANwA6ADwAPQA/AEEAQQBCAEIAQABBAD8APQA8ADoANwAzADEAKwAlACEAHQAbABgAFgATABEADQAIAAIA/P/4//T/8//x//P/8//y/+//7f/q/+j/6f/q/+3/8P/0//f/+f/4//r/+f/6//3/AQAHAAsAEQAUABYAGAAYABoAGgAdACAAJAAnACoALQAtAC4ALwAtAC0ALgAxADIAMwAzADMAMgAvACwAKQAnACUAIgAhAB8AHQAaABcAEgAOAAgAAgD9//j/9v/z//H/7f/p/+X/3//c/9j/1v/U/9T/0//T/9P/0//R/9D/z//P/9H/0//V/9f/2P/Z/9n/2v/b/97/4f/k/+f/6P/r/+3/7v/w//L/9f/5//3/AQAFAAgACwAPABEAFAAWABkAHAAeACEAIgAjACUAJAAlACQAJQAlACUAJQAjACMAHwAdABoAFgAUABIAEAANAAsACAAFAAIA///7//j/9v/z//L/8P/v/+3/6//p/+f/5v/l/+X/4//j/+L/4P/g/9//3//g/+H/4f/h/+H/4f/h/+H/4f/i/+P/5P/m/+f/6P/q/+z/7f/v//L/9f/3//v//v8AAAMABgAIAAoADAAPABEAEwAUABUAFgAWABYAFgAWABYAFgAWABUAEwAQAA4ADAAJAAgABwAGAAYABAACAAAAAAABAAAA/f8AAP////8EAAEA/P/6//j/9v/3//j/+P/4//f/8//x/+//7f/u//D/6P/o/9z/2//Z/9n/0f/f//X/4P/7/+L/9f/w//n/9P/9/wYA8v8JAAEAEAAIADgAKQAlABcACwAjACQANgAtACwAHgAWACMAMwBHAEAAOAAsACIAKQAsADEALAAdAAYA/f/3//n/9P/t/+H/1f/R/8//0P/P/8n/w/++/73/v/+//73/uv+3/7T/tP+1/7X/tf+0/7b/t/+7/73/v//C/8X/yv/N/9H/1//Z/9//5P/p/+7/9P/6////BgAKABEAFgAbACAAIwAoACsALQAwADMANwA5ADsAPQA9AD8APgBAAD4APwA8ADsANgAxAC0AJwAkAB4AGgAXABIADgAJAAYAAAD9//v/+v/5//n/+f/6//r/+f/6//n/+f/4//r/+f/4//f/9v/2//X/9//2//f/+P/7//3/AAAGAA0ADgANAA=="} (15:48:00.076) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.097) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.117) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.138) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "vCYhJNogyBz0F9MSxg3jCNwDSf4v+BDyVOwz55/iiN4t27XYVNcW1/3XzdmT3fvig+jS7iX2eP7wB70Q5BdNHp4jBye3KHIo8iYNJWIi1h6sGiYWjBHgDBMIDgPW/WX4/vKi7afoQ+Rp4Ffd/dp72QDZg9nm2n/eueP86APvK/Y1/nsHMxAzF30dvCIwJhUo9CdhJl4ksCFPHngaJxbAEUoNtgjtA8D+Xvn787zus+kZ5RfhAN6q2wXabtnA2dva/N0I4yvox+2L9EP8XAVdDqIVAhyBIVolnSfyJ5omkiT5IaQe4RrIFnkS8g2LCTUFeQCE+3v2c/Hh7IvoSeS94CDeKdwi29naLtvH3Mbg6eXm6qvwmffY/zgJbhEaGBAe1iIiJrAnFydhJTEjQiDJHO0YvxSjEJEMhAgZBFr/lvqr9eDwYuzR55njXeDk3RPcJ9vd2lrbYN3L4f/mG+wU8hT5hwHQCooSzRh4Hu4iyiXKJtwlASS4IcoebhuoF7UT0w/qCxIIAQSh//X6xfWK8M3rTedf4zjg0t0+3NnbCNxT3N7dB+Ja5xfsZ/H599T/xwjCEE4XFh21IbAkCCYtJS4jtCDgHckaOBc7EyEPOQu/B/sD8v+C+9H2I/Km7cjpYuYa40ng/N0E3eTcD90N3n/h0Oac66fwxvbc/WQGxg6TFXgbYCDKI7IleSW5I2ohnR6jGwYYEBRbEKIMAQlfBWMBF/1x+KzzCO/b6k/nV+Tw4Q7gIN9Z32rfid/C4Tjmo+r27hz0JvopAu8KhhLvGEkeLCIcJTsmYSWFI4sg+RyCGfQVNRIpDuwJ+AU3Ap3+pvoJ9mbx+uzV6CflL+Io4FbfLd/B3jPesd9M5P7pTe809AL67QFGCy4ToRlfHk4izSMtJD8iACAZHV0Z5RWEEUcPRQzyCJcF4wGn/v36jfUz8H7rEugG5urjQeKT4M3f8d5i3n/gxOaq9V4AtQf+DVARLhS3E7YR0BHnEKsQnA5uDvYQuhLuE/0S1hE2EAUNTAloBKj/zvui+MD10fME9Pryb/Hd74nsR+kn5jblhOJ84GvkNezi/8YI+w0dFEkTyREFC2QI9wlKCTsKVQsmEswXNBhdF/USxQ1hBY//b/zY+Pb3XPlg+0T7efph+T72GfIW77rsr+lU6D/oj+gl6K7qRPf2BwwLfw/3E0kSUAkABVAH9gbGBckIoA3mEtESJRQkEtwKJgUlAab+hvrW+hX+fv0h/kP+9v3q+tH15PPU8XLvbe337FLtZuuf6g3trPLzA7wJxw0yFHoS0ApBA9sEHwXFAZgGLwzJDq8QkxNjEnkLZAYWA6z/Q/tR+9/9rv1c/j8AZQHx/Sn5jvYO9Ujxoe7J78LuRO036nzrUu2e8EgFRAn+Dg0VxxGxCyoChAXjAun+zQVtCMMNyQ32E8YSEQueCbsEUgAc/DT99v0t+gD+vP5U/679Kfzp+vH1avbu89XyV/IN72XuTOox7HTse/VPCL8GExKSEwoQBgmRAsoGq/57/+MGPQYcDI4NlBQ5EPsMKQ0QBpcA6fzQAK/9dvcb/2L/VP+5/isAt/7q+G74VPXu9DT1dvKo8ivxQu6a5/PmVvKCA+MCuguKEpEN7AbiAY0Cu/vZ+uT/GwIHCJkMKxQmFZwTJhP1C1kHNAFn/gL7w/mD/I39aAHRAYADKQPV/z/+ZPkg92jzlvJb83f0L/hx+5f/PgINBeYGQwXxA7EBOP8N/p7+X/8FAVEDHQUbBvkFSwU9A93+Afrf9Nnvg+mM5q3ppgOs+ZIF2RAdCscLYQRgDKj/n/vZ/x/6nf19/ykI+gtLDFsU3xHIEQAPBAsYBnn+0/40+pP2u/fD+EP87flo/6gCaf+M/pL/A/9m+ZD4I/qe+Fv4BPsOAGwAoQHBBAoEsQEkAZD/P/0d+/D6xfp5+vD7TPzq/Wr/0wBkAZEBuwKUAU4BrwGaAb0BUAJnA9oDzQNBBFAEZQNiA78C5AEDAVAAfwCn/1//+f9KAC4AgACqAHMA1f8//9f+2f2D/Xz9Wf2g/d79xv2t/Zf9gf0t/R79o/3c/d39uf5V/5L/GgBbAQYCygK0A30EVQUGBcwEWASzAu4A/P7f/Fv69/dQ9hH2VfWG9Q=="} (15:48:00.159) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.179) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.200) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "pACDANX/DwCwANz/sQBYANb/rP/Q/4EAQQCf/6//nv8IACwAYQC/AK4AsP/tAGgAi/+q/6AAWQCX/1gA4P95AEYAPgCEAFMAEABQAIEA0v+vAL7/mf8jADYA8f9UABcAYgD5/zEAOwBWAHQA9f8wADIAJQAhAEgAvv8EADEAWAAzAEEAIAETAJkAlQADAEUA//8aAJ4A+P8HANf/VADG/yQAGwATAAwA+v8OALv/if+7/5z/aP9x/2z/c/9E/2v/W/9P/2j/Rf9l/zr/p/9C/y7/Tv+e/2j/iP94/5D/wv+E/3v/3f+d/8b/9//W/yQA0/8qAB0AFAAcACQASgAtAEAAHgAdAB0ALgA4AEwAOAA1AD8AdABmAEgASgBXAGIAKgBRAC4AKgBqABcAQwA9AEgATAA7AEAAUABKADcAOgAoADUAJQAfABQALgAiABwAHwALABAAKwAWACIAHwARAAwAAAAZAP3/GAADABAABAAEACEABAAOAAwAEQAEAAQABAAQAAgADQD+//7///8WAAQAEQAQAA0AFQAMAAIAEgATAAoADQAeAAEAFwAfABsAJwAZAB4AIwASAAQACQAFAAEABAAFAAoA///7//3/+f/4//3/9v/2//P/8//x/+z/7v/s/+r/5v/q/+P/4v/l/+L/4v/h/+L/3v/j/+L/4P/g/+P/5f/n/+T/5//r/+z/7//y//X/+f///wEABgAKAAkADAAUABAAGAAaABsAHgAeACIAIAAgACEAJAAjACIAIQAeAB8AHwAdABsAHgAXABoAFwATABEAEgALAA0ACQAGAAYAAAABAPv/+//3//f/9//u//P/7P/q/+j/6P/i/+D/4//c/9//3f/c/9v/3P/a/9z/1//a/9//2v/g/+H/4v/m/+z/6v/v//n/8P/+/wAA/f8JAAUADgAKABYAFQAXAB0AGwAkACIAJwAlACoAKgApACwAKAAqACUAKAAhACAAIAAZABoAGwAVABQAEQAOAAwABwADAP3////2//T/8//t/+7/6f/o/+b/5f/k/+D/4v/e/9//3//e/97/3//j/+T/5v/o/+z/6v/s/+v/6f/r/+z/7f/w//H/8P/y//X/+P/7//7//v8BAAgABwAKABIADwAWABUAEgAfABAAHQAbAB0AGwAgACUAGAAjACUAIgAZADsAFgApACkAGgAlABUAIAAHAB8ADAAEABUAAQAKAAEA/f/8//r/8v/s//z/4//4/+X/8f/b/+3/6P/Q//L/zv/q/9b/3//g/87/6P/Z/83/7v/E/9//7P/F/9//5f/e/8//CADL/+T//P/7//3/7v8pAPb/GAAhAAkAJAAUAEQAGAAhAEAAOQA6ACwAPwAgAC0AVwD+/6MAlv/5AAP/YgMtAND+oAMJ/DoDQ/5DAnX+0gClApb8igGB/gICNP6zAIMAfv4aAQMBSQCq/Un/BQHa/gL/1/4r/4QAPP9yAQf/8P5k/8MA7P8UAQMByf1o/v/9Of75/H7+Ov/h/ff+3v+i/+8BJgIZAv8A/QCz/47+cQB0/lf+Uv8Z/8ABlQDGAJIAgQD4/nD/4wCV/94AJAAWACn+MP8t/wP/WP8DANP/gv4dAdn+wv91/zAAEf+V/5MAD//oAOwAMgGUASEBYAFcAK4AEgG7/8gAUgBVAN//EwCYAGL/vwF5/xMBAAApAcH/P//nAEf+fQB5/pz/qv72/igAz/5DAEr/NgAAAIn/uQAi/zQBgP/wAM3/gP/9//3+TAAv/3QA5f8RAIMA+P5OAHv/Wv9R//3/d/87/4X/Kv5t/t39Y/w8/Nz78vog++L7q/tR/eP9TwA3A4IE8QZ0CcsJYwqSDJoLDQr6CVoIIQVaBBIDXQBl/qD+gfzH+tj60flN91v2ivUx8pLvVO6C7EzqQuqs6pDsffps/tEGChE5Fa0Z1hoOIPYYJxdMFRcNjQcpAkgAtfk8+gX9z/0BAzEIGw6TEEYSGxLTDXQH1f9R977uxug34gPez9ps2YHZzdrg3gHiC/B8/YsD1hESGgMeRB+iIlsfnBb9FYcO0QUrAEf+bfo793D7s/ym/+UFQA1qEDkSgxQ1FfkM2AUxAZD0ge6w6iHmCOLY3zrgut0w4Frh7+PC6Q=="} (15:48:00.220) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.241) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.261) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.282) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.302) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.323) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.344) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "NPp/9gPzAvAz7Szriunl54HmkuXd5FfkyeOo4wfkQeQC5Xnmkege7EvxZPYa/OQCSwnCD+MVLhucHxwjhyQHJPAiHiETH3oc7hlQF08UWxHzDWsKtAZDA5P/kfvW9zv0LPFv7hzsOeqD6CznJeZO5cLkXuRZ5KDk/uTO5TbnJekV7A7xJvZf+7IB7Ac+DisUjhnaHXchPyPLIvYhEyA8HhUcsBlEF2AUrhFXDvEKXQe9Aw4A+fsC+DT0NvGx7nzssOon6drn5OYI5hjlieRE5Ezkk+RJ5ZrmXeim6w7xFfa5+3ACuAilDwAWTRuLHw0jjyQhJOQicyCrHloc7Bk3FzsUVBHbDX4KjgbuAir/8frW9vjyKPC47brrJeqp6I7n1+bf5eDkSeRB5I7kS+WP5mLog+t68bL32fxHAwsJpA/UFW4a+R36H20h5iDlH2YdYRsWGjsYfxZ/E7UQGA3wCVMGWQIL/+P6avcw9Nrwuu3Z6zLq2efK5UDkvOMp4/PimOLx4ZDi1+MD5oLp9PLs/bQH4Q9PFOkYfxuQHVIc7hq4GPYXGBipF3UY/xdqGdsYPRcAEbYJIwb8AbIA+fh+80HwUfDZ8Dnt/Ouk6dvp6ud75jzkseAb4EngGubQ6zjx5vf4BmQU9RgKF3UOzgynC3oOFgzzDRER1xigH0seuxrcEEUMYAhXBb8BZv8eA4sGtwnPBVH/ffi78yLyde5L7G3pVOmg6UXpjedV5VvjsuN35TPqfu1q8gH+lw+mGrQdXhSkCosFNQe7Dd4QxRUpGF0efB9iHX0V/QoSBWgCkwUEB+AIHQnnCGkI2AJJ+8zwcuqC6APq6+uO6g/o2eRF5aTkOuNg4DDgAORe6sjzbAK1FHweRx9bEj8HBgCUBB4PahgkH6QeUB/xGzAZkBIsC6EEzQCbA1oIeA3hDNsIKwN5/H32de9m61DqF+3g7kTt/edM4l/gLt/d4DHhvOPQ50HuzviGCSAYNh7ZFwgKaQDW/kwK8xUfIScjJCDGGSgUixEWDlkL8Aa7Bg8IUAsZDcoKDAZl/pD4M/OD8LjucO5l74rtFews5Qnhv9rj2XLdJ+L/6SHuVPWc/yERqByDHgsTbgS/+S78qgl7GjgnwChZIn0Whg4GC94NRA5ODAEIxwX+B6oLugx6Bwb9+PHE6oPq3Ozl8OrvK+yy5TLf1tyS29rfAuJp5ozpzO6a+L8KihzsIwMd3As9/Lb1JQABEuwkxCviJ6waxA1oB+cJ2RAZEwwQEwj4AVwB0gS1COkFof0e8uDpkehL7Ijy0vJN7/fktt0k2s/ct+Jg5y/s9+0v85QAuxSUIjAj/RV7AwD1CvYGBSEb+SrhLSIksRKoBWUBXAgBEVIVhBHuBxYAAf7BAfQF3QOq/Cjx+ukP6JjsLvGl8oHuz+XZ3q/ab9y64KTnx+3E8uf7aAxCG4QhGRoSC3H6n/MB/NkOESQiLkksUR0QDIUAdQBiCeISFxb2ELMHEADz/Df/7gA7ABT6sPGp6r/nO+mc7HbuW+yl5p/fvtvz2/DhqekK85j7fgtMGbMhyRysDif+bPPO+IsIdR/DLW4wgSPzDz4AKvzcAhgO8xW4FaINGQQl/Xn8Ff4F/x78Lfa479LqsOkD6gnrs+mc5lrhud0R3XPgYufk71r69ApXGv8itR51EIf+sfJl9t8F/RydLEYxSSUjEkIBIfs8Ac4MwBYLGMwQSgZq/Iv5yvmg/F78a/ix8fvqz+cE55/ocehG54riRt8b3T/geuZS8FL8mA4RHWsjIhz+C6X6jvAu9+cI1iB3L88xIyNrDun9c/p0AqIPUhmjGagQGgQ9+tj3N/nx/Gr8h/j18J/qMef25hjox+e15YzgNN2v3PThYeqs9MIBHxSRH/QiahcHCOb2SvIV/IEQTiY0Mf4usB1uCbv7UvsoBXASTRrtF0ENjwAu+AP3k/ls/W37K/Y77WnnUuSu5aLn/eh354zjK+CM37jjxOs59xkJ+BuYJeQijRFG/4PviPLEAiUb2y3UMrEophO/AAr5gP0IC+YWahonE9EG0fsw9sv2IfqF+1f4JvEv6jvlo+SE5sDnJOgV5IPhh99s47DqxfT/Ar8VmyBVI/4WdQbf9SHxx/xAEQ=="} (15:48:00.364) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.384) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.405) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.426) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "7f/y/+f/6f/Z/9f/2v/M/93/1//e//H/5P/7/+b/8//n/9L/3//R/87/1v/S//T/0P8DAP7/5v/5//X/9v/m/+L/AQDu//D/EAAOABAAIgAiAD8AKAAxAFUAIwA+ADkARwBZABMAZAAzAC0ASgBCAE0AQgBWAFYAQgBFAEsANAA6ACUAJwAdABwAKwAeACoAKAAkACIAGwAXABEABwAIAAcACQANABAAFAAVABQAEwARAAwADAALAAwADgAQABgAEQATABAADwAMAAcACgAIAAoADQAPABMAFAASAA8ACgAGAAUABAAEAAUACgAIAAoADwAPAA4ACwALAA4ACgAJAAwACwAMABAAEgAVABYAGAAXABQAFQAPABAADAAJAAoACQAEAAAAAQAAAPz///8BAAEADQAPAA8ADgARABMAEAAMAAoABwAIAAkABwAOAAwACwAMAAsADQAJAAgACAAGAAQACAAMABMAGAAZABkAEgAVABAACwAPABUAFAAVABAADgALAAMA+v/+//3/+f8CAPv//v/9//X/9//2//X/9f/0//f/+P8AAPf/8P/v/+z/7v/s//D/9f/0//X/8//v/+7/6//t/+//8f/z//b/9P/0//b/9P/w//j/+v/6//v//v8BAAUA/v8BAAMABAAAAP7////7/wIACwAMABUAFAAKAA8ACQAAAAkACwARAA8AEQAQAAoABwACAPz//v8EAPT//f/8//P//f/1/+//7v/0//r/5P/p/+v/6v/p/+j/6//u/+L/5//w/9z/5//s/+D/6f/r//X/6v/x//7/8v/5//v/AwAIAB8AJgArADMAMwAiAB0AFwAvAC0AFwA1ABUAQQBfAKr/3f/cAFQA3v/xAOYAvABB/6IAHAGuAG4BwAJYAk4AtgGKAOn+H/6P/nX+/PwR/bcAIQCc+6z9zQMiBxUFHwTzA6H9Iftn/yQCAAYGAKwGrAg2AtMAafUqALv5NPyhBI0GsAO4+ir5Qff+87v06vQ293n0h/Pw7b7sQez0B84jHxJDEGIM6fvh7Xb7UBJ+EZUZ0B+ZFFYIMQHeAYf/6v4tBdMJdQ+GC9EGWASV/dT11fLq+K776fxNABP8i/Vm7XLrrezr7iXx7e587//vDetb7k/xlAyGF5sPbxHDCrgAzPhwBI4P0RGjHCYczhOODD0FKwTLASkCAQk4DXcNFghTBmoDIv0a+dz1+/f++Yn3a/RO8azxVO++6PnnRuRi4SLgc+YH8S/5zwwmFjgV4xHsBcf75vYr/XcLJhgGID4ipBsQErEKEwdCA8cG9gx5ELsPfwwWCVMEsv17+S355Pn0+b/53fpu9DXwSvCY7PXojOsT7h/se+mx6B/mYuhk7AHyW/rVCXwUihLREr0NBQO2/U4DmwrDET0bxiB5HQkTlQ6xDGwFuwX8CroLIwu8CkQHAAIT+t34RPZ09Yn2D/ff9evzSvMv8MzrL+hd6ETndehr59nqTO2x66TwjfOw+rsH5xCiE/0U3A9sCc4BBQkYDyQVKh76If8cAhSoEnUMvwXYBBsIYQnyCowMpQlpAp37tvh49K3yGvSB9XP0IfQr8vbt9Ohw5WHmTeUB5hHo1OsF6W7scfDA8Z36+wenD1MROhYIEokLlQdjDvgRnhewHrsfTBthFWUVLBDSCccJMQo+CEEIIQnkBGIBPPuy+BT1VvKJ86zz+fJL8XrvLOxp5z7mW+bb5Q3nlOd86xvrxevX8avzN/fdAjcKIgv2EYwUpxKFDxcW0hYKGCIbbBw2GlUUThbvExsRXg6WD+4L7wc0CZUDhv6S+of5VfYK8xL1UfKu8HXvKe7z63np3+nt6GDnp+nC6rvrkOxB7yPydPJ39Mz4mP7lABQEwgltDGoNshC2FMsWjBjyGucbzxmFGMYW6BXDE6ISIxAfDZUL9ge7BQwD5/9l/Rj6XvhL9hn0fPJi8BnvBu0L6+rp1+iQ6FXoHui953jnJugs6TzqbusL76vyLPf0/AACQAftC+cQZxXcGW4dqB9VIdUhviG3IH0fHB5mG2wYQhWFEXwNIAkFBWwAovtd91Xz4O+A7HbpZedS5fXjxuJt4t3iQuP843zkhOV05kroS+oj7ITvpPMi+D/9tQLBBw=="} (15:48:00.447) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "dAzJEaYWWRsBHywh4iI2I8YikCHeH50dsxpKF6kTFBD/C/UHBQQHAOf7+vcm9MrwC+5L6xrpa+cb5gflRuRS5KPkM+X15eHmsufb6Mrqguyq7vbx8vXg+rr/uwQ7CgYPLRQKGXYdqCCAIqkjfSPHIlIhkR8tHTga5BZ7EwsQUwzbCCoFFAEw/XD5zfXF8rvvEe206m7oy+Y55Trk1ePZ4w3kc+T95L/lQOcQ6TDr2O0y8VL1Rfq8/r0DGgndDRQT9xdjHIUfriHXIrYiRyIbIWMfBR34GZsWKxPoDxkMbgipBIwAqPzf+HX1bfKc7//s1+rL6N3m4uX45K7kteS95BLldeV15inoG+oa7JLugvFd9Tz6ef5/AyMJmg2nErIXIhxlH6gh8yKmIh8i9yBXH7kcpxnBFm4TTRDcDIIJ0AUPArv+H/vx98X0FPIg757sk+oy6P7mI+au5UDlm+SB5YPkKOfL6PHoU+xX7Y7v4vPQ+uD8RANtCf4LuhP1Fy8dQiD+IcQiViEGITgfAR5IG9sYlRVfEtIPYgzyCe4FWAKW/pX6OPfl8wrx3+3Z653prueZ5rbldeUU5Y3lv+UE5gHn+Oc/6cPqpewB71PyW/cJ/CgB7gbjC4URyxaPGyofOyEnIqIhqSALH20dQBuIGNsV5hISENgMvAlABnUCmf6R+sP2OvNQ8Kbthuvm6VHoJud55j7mLuab5hvnl+eC6KLpQOvw7M7uD/HY84/3v/vD/1QEDgmfDVoSqxZKGtgcVB7iHr0e4h24HDEbDBnXFj8UkhHjDrcLdggWBUwBpP3v+Vr2KvMy8KXtk+v96b7oAOie52nnpece6MHopenP6gTsSu3C7kzwYPLY9Mj3uvuI/9kDvQhbDUESyBa9GtEdoB+EIDAgTx8CHiEcAhpVF5sUsRFxDkcL3gdRBKcA+vw8+bv1jvKk7y3tFeuT6UfoYudO51DnwOfJ6KLpt+r160rty+5U8M/xY/Mc9W330/qx/p0CkwaoCi0P+BMwGE8bLR3FHRYe4xynGyUa0ReTFQkT3BCvDjcMUAk7BgEDrP9z/BH51/Xh8lLwRO6Q7CPrJeqF6XPp1OmI6mfrX+yC7dzuiPBe8kj0QvZB+Gb65Py4/7kCzgXiCPMLEA8cEr8UsBbWF2EYOxiXF4YWERVVE1cROA8DDasKNgiiBfgCWADb/Vf7w/hO9gD0/vFL8PHu9+1N7frsHe2X7S/u8O7f7wXxb/IA9Kv1Xfce+e36qvx+/lEANgIzBEwGjgi4CrIMdA4AEEoRQBLaEgoT2xJGEk8RBBBdDnYMbgpKCBIG2AOkAXX/XP1V+275ovcD9pP0VfNd8rnxaPFU8Xnx1/Fv8izzFfQV9TD2cvfG+DT6rfsb/Yf+7v9UAagC5AMFBf0F5wayB2MI+QhgCawJ7QkbCiYKFQrrCa4JUwnoCFwIlwe1BrUFiQRcAxICvgBz/yr+7vzB+6z6qfna+Cn4j/cz9/r27vYF90H3ofcf+Mf4c/ks+vT6vvuR/Hr9VP4o//7/0gCcAWACDwOlAy4ElQTfBAgFIgUxBSIFCwXoBLEEgQRLBA4EzAOMA0ED5gKGAhgCpwEoAZMA8/9a/7z+Hv6H/fL8avzv+4L7JfvT+p36gfqC+pr6zvoY+2/72ftd/Pz8o/1V/vX+m/9GAOUAfgETApkC/gJsA7UD5AMhBC4EGgQGBNMDlwNJA+oCtwJbAvYBiwFeARgBPwEXAeP/yv+q/+X/a//x/mb+W/6b/lL+df5L/k/+Xv5p/qf+av55/sH+7/4r/yv/R/9//7H/uP/8/x0AOQBWAH8AqAD1ABkBJwEGAR8BUgF0AZcBMwHrAIUAhgCfALMA5ADTANYACgFsAO7/tP/m/5kAxQDdAfICbAU9BE39XfYp9Hb5igFuBmQF9f+t+x78lP97A8cEyAJ4/lD7Zft0/TH/ff8W/0X/fP/h/wsA///Z/8X/AABEADEAzP/3/qL++f6L/0EAhQCtAKUAzQDSAIAAVwB3AOUAOwEiAZMAs//k/vz+/P8VAdYBuwESAesAggExAmACfgFTAEj/Zf7P/VX+sP9UAdoBPAErAJ//+P+tAOkAVgAdAGwA5wCYAEYATv+V/ZD7Rvoz+g=="} (15:48:00.473) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.488) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.508) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "P/tV+fP3O/cs9473+fdt+PT4mPlr+if7pfuk+xH7SPpe+bv4oPjp+ML5yvrv+1L95v7pAEsDJgY/CfgL/A3fDq4O2Q2NDD4L0QkXCB4G2wPcAZUAHwB0ABgBygFQAooClQJZAr4BqAAG/xr9IPtr+Sz4Pved9jT2EvZz9kD3RPg++cD59Pnz+fv5Nfph+nf6cfp++uT6rfv1/Jb+YQBRAjgEaAbFCAsLDQ09Dp4OZQ6mDcYMlwv1CfMHiQVIA5IBhgAbAOz/zv/A/67/nv9j/97+7/2P/OX6I/ma92T2avWg9AT02vNA9B31NPZK91j4c/nC+hb8Vf1q/mL/xwDWAqQFtAhPC/gMgA1EDcIMSgzPC9EKBQlmBo4DVQEjABMAgAD8AFgBpgEjArEC9gKDAjUBY/9w/e77vvq3+aX4gve/9p/2P/dn+HX5Kfpq+nT6ifqz+uv6zvpQ+q/5KPkz+cL5n/qi+4z8ov3q/nMAOgLSA2kF6AaSCEcKdAsUDPULiQsvC84KYgpfCcMH1AXvA5ICmwHaABAAKf+H/i3+Ef7K/fb8tPtE+vf42PfQ9rz1pPT18/PzlPSp9cn25/cI+Xf6ffzi/mEBxgNUBskI7QqZDHANzA29DVQNeAzDCpsIKAYPBIcCawHQAFEAHgBiADkBYAINA/cCBwLAAJP/af4d/XD7n/kf+Dr3IPde97n3Bvhi+Bj5//nO+k37PPsN+/T6D/tY+3f7m/vO+1X8Qf1B/lj/VQAqAQYC2AK9A20E9gRwBewFhAYOB54HAgggCCoIDwjgB4UHyga2BVwE7QKIATIA/f7P/bL8yfsf+6f6PPrq+bT5oPm0+dn5GvpM+nf6yfoa+2X7wvtB/Nf8if1Y/k7/hAAIAtQDvgWpBzwJYQowC6MLwQtfC28KIwmVB+IFQQS/AngBkQDl/3r/OP8A/8n+jP47/sP9Lv1q/IP7g/pv+Xf4tPcL95X2b/Z59ub2nfdu+Ev5Nvoj+/z7w/xS/bH9D/6T/i3/x/9+AGcBggLDAyEFggavB4EIIwnSCU8KYwoZCpIJzQi1B2AG/wSbA0YCGgEZAEf/hP7H/RX9bPzA+wz7afrd+Vz5yfg++O/3wPe69+L3NPjA+Hr5SvpA+zv8Rv1O/jX/LQAOAbYBTwL4AosDAwRaBKsE+wRQBbgFFwZqBqYGwga9BpUGewZABuQFcwXdBDoEkAPrAk8CvwEzAawALAC4/1X/7v6C/hj+sP1W/QL9tvx4/Dz8Fvz/+/T7/vsc/FP8gPym/NL8/vw9/Xb9rP3Y/Qv+Ov5g/o/+vP7g/hD/Qv9t/5P/t//c//n/FQAwAEUAWQBkAGwAeAB/AIoAlgCfAKYArwC1ALkAvgC+ALwAugC4AK8ApACfAJgAlQCUAJUAmQChAKoAsQC1ALsAwADBAMQAyADKAM0A0ADSANEA0ADMAMUAvgC1AK0AowCXAIwAgwB4AGsAYQBXAE0AQQA4AC0AIQAYAA8ABQD9//T/7P/j/93/1//R/8z/xv+//7f/rv+m/57/mf+V/5L/kf+T/5b/m/+g/6T/p/+q/6//s/+3/7z/xP/N/9b/4f/t//r/BgAUACAALgA7AEYAUABZAGIAawByAHkAfwCGAIwAkACUAJgAnACeAJ8AngCdAJoAlQCOAIYAfgB0AGsAYQBYAE4ARAA6ADIAKAAeABUADQADAPn/7v/k/9r/zf/C/7b/q/+j/5r/kv+L/4X/ff91/2//af9k/2H/Xf9a/1j/V/9Y/1j/Wf9c/13/X/9o/2//cv92/3z/fv9//4L/iP+Q/5f/oP+q/7L/u//D/8r/0P/V/9v/4P/k/+n/7v/z//r/AQAJABAAGAAfACcALgA1ADsAPwBEAEgATABPAFIAVgBZAFsAXABeAF8AYABgAF8AXgBbAFoAVwBTAE8ASwBGAEAAOwA1AC0AJQAeABcADwAGAP7/9v/t/+b/3f/V/83/xP+9/7b/sP+r/6X/of+e/5v/mf+Y/5f/l/+Y/5r/m/+d/57/oP+k/6n/rv+z/7n/v//H/9D/1//g/+j/8P/5/wIACgASABoAIwAqADEANwA/AEUASwBRAFUAWQBcAF8AYQBiAGIAYgBiAGIAYABfAFsAVwBTAA=="} (15:48:00.529) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "UABNAEgAQwA/ADoANgAxACsAJgAgABoAFQARAAwACAAEAAAA/P/6//b/8v/v/+z/6f/m/+P/4P/e/9z/2//Y/9b/1f/T/9L/0P/P/8//zf/N/83/zf/O/9D/0f/S/9b/2f/c/97/3v/i/+T/5v/n/+f/6f/o/+n/6f/q/+v/7f/w//L/9P/2//f/+f/7//3//v8AAAIAAwAEAAYACAAJAAsADAANAA4ADwAQABEAEQARABEAEQASABIAEgASABIAEQARABAAEAAQAA8ADwAOAA0ADQANAAwACwALAAoACQAJAAgABgAFAAQAAwABAAAA/////////////////////wEAAQACAAQABQAHAAkACwAOABAAEwAWABkAHAAfACIAJAAnACoALAAvADEAMgAzADUANgA3ADgAOQA3ADcANgA0ADIALwAuACsAKAAkACIAHgAbABcAEwAQAAsABwAEAAAA/P/4//T/8P/s/+j/5P/g/93/2f/W/9P/0f/N/8z/yf/I/8b/xP/C/8H/wP++/73/vP+8/7z/vf+9/77/wP/C/8P/xP/G/8j/yf/L/83/z//S/9T/1//Z/9v/3v/h/+T/5//q/+7/8f/0//f/+v/+/wEAAwAHAAgACgAOAA4ADwARABIAEgATABMAFAAVABYAGAAaABsAHAAdAB4AHgAeAB4AHgAdAB0AHQAcABwAGwAbABsAGgAZABgAFwAWABUAFAATABIAEQAPAA4ADQAMAAsACQAIAAcABgAFAAQAAwACAAEAAQAAAP////////7//v/+//3//f/9//3//f/9//3//f/9//7//v/+/////v/+/////////wAAAAABAAEAAQACAAMAAwAEAAQABAAFAAUABAAEAAMAAwADAAMAAgACAAIAAQAAAAAA///9//3//P/7//v/+v/7//n/+f/5//j/+P/5//n/9//3//b/9P/2//T/9P/y//L/8//z//X/8//0//T/9f/2//b/9//4//f/+v/8//v//f/+//7///8AAAEAAAACAAIAAgAEAAMAAwADAAMABAAEAAMAAgADAAMAAgAEAAgABQAJAAkABwAMAA0ABAAFAAkAAgAHAAYACgAEAAMABgAJAAkAAgANAOb/7f/4/wQAAgD8/wAA+v/1/+j/5//7//3/6f/y//P/8//o/9z/5v/0/+L/4P/t/+D/1//b//T/7v/9///////z/9P/2P/k//X/4f/i/yUA7P8RAGYAKwAEAKH/wf/S/+P/AgD4/yAAvP98/7X/z/9iADUAbAAaATcAdP8m/xn/L/8Y/yD/iv/w//z///85AHoAdgApAEkAQQDu/1v/Ef9J/2r/xf+b/xcAVwDG/3n/rACbAIIA5ADrAPcA4P91/5b/r/9z/zb/CwBSADcA/QAzAn4COAJIAkwCbgF1AGEArwAGAHD/dP/B/wkABQBOACEBkgELAYIAmQBIAHX/M/+J/8n/SP+3/i3/n/9s/zX/Zv/p/8T/JP8y/+n/vv/g/uP+hv8x/9v+Gf+C/7f/Pf8u/5z/c/9b/17/hv+5/1f/a/+e/9L/mP+Y//3/zf/o/8H/t/8VAPr/x/8nAFsANAAbAGsAgABVAGkAawCdAIgAlQBvAIkA1QBuAHsAkACqALsAdgBkAH0AkgBzADYAYQCCADkA/f8mAHMARwAPACwAMgDV/7n/5P8RANv/zP/j/8r/4v+9/7//p//O/8D/ff+b/6v/uP+X/5T/w/+7/5r/o//P/9r/o/+X/8P/wP+q/7v/7f/0/7//4f8FAPb/3f/9/yUAEgDk//v/KwAlABsAIQBHAEcAJwAtAEEAQwAtADEASgBUAEEALQBCAE0ANwAyAEMATAA5ACMAKgAqAB0AHAAfABoADAAFAAsACAAFAAEA8//n/+T/1v/P/8z/yv/J/77/rf+v/7f/tP+w/67/s/+o/6L/n/+j/6z/qv+q/67/sv+7/7X/uv/C/8r/zf/K/9L/2P/g/+f/7//3//j/+f8DAAwAEAAYACEAKAAuADkAOgA8AEMASQBKAEwATwBVAFQATgBRAE4ASwBNAFEAUQBMAEgARgBDADsAMwA0ADAAJgAfAB8AGgAWABYAFwAUAA4ABQAHAAcA///6/w=="} (15:48:00.549) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "9v/x/+z/6v/n/+D/3P/a/9j/0v/Q/8//zf/K/8b/xf/E/7//vP+8/7r/uv+7/7z/vP+8/7z/vf/A/7//v//A/8L/wf/A/8L/xf/I/8r/zP/P/9P/1v/b/+D/4//o/+r/8P/1//n//f8DAAgADAARABUAGAAbACAAJAAlACcAKwAuADAAMQAyADMAMwAyADMAMgAwADAALgArACsAKQAmACMAIgAhAB8AHQAcABsAGAAUABIAEAANAAsACQAIAAcABgAEAAMABAACAAIAAgABAAAAAAACAAEAAAABAAMABAAEAAUABgAHAAgABwAKAAsADQAPABEAEwAVABcAFwAXABkAGgAbABwAHQAdAB0AHAAdABwAGwAYABcAFQATABIADwANAAsACAAEAAIAAQD///v/+P/1//T/8f/s/+r/5v/j/+H/3//e/9z/2//Z/9j/1//V/9P/0v/Q/87/zf/L/8v/y//N/87/0P/T/9T/2P/Z/97/4f/j/+X/6P/q/+3/7//x//P/9f/4//r//f/+/wAAAwAGAAcACAAJAAoACwAMAA4AEAAQABIAEgATABQAFAAVABYAFgAWABYAFgAVABYAFQAVABUAFQAWABUAFQAVABMAEgASABMAEAAQABAADgAMAAsACAAHAAYABAACAP///v/8//r/+P/3//b/9f/z//L/8f/w//D/7//s/+z/6//q/+r/6//p/+n/6v/p/+z/7P/q/+v/7v/s//D/8f/3//b/9f/5//X/AQDt/w8AFAD6//H/CAAdAP3/9P8KAAsA/f/7/wUACAAIAAcACwAJAAoACwAKAAgACAAJAAgACAANAAgABgAFAAMAAAAAAAMA/f/8//z/+v/z//H/8//z//H/6v/y/+7/8P/s//D/7f/s//X/7//v//H/9P/1//P/9f/x//f/9v/2//n/+P/3//j/9//5//z//v/9/wEACQAKAA0ADAAXABsAHwASAAoAKwAgAA4AFAAXABcADQAMABUADQANABMADQAOAAYAFAAGAA4ABQAHAAUACAAOAOn/+P8RABIA+P/8/wsA+f/v/wgAAQDr/w8A9P/k//X/BAD2/+b/BAD6/+b/3/8EAAUA6f/h//D/+P/n//D/CAD//+D/7v////3/7v8GAAUA9f/9/wcAAwD3/xcACwD2/wQAEAAtAPP/+v8dAA8ACgD6/zUAIgD0/wkAGAASAAsAPQAYAP3/CgAcABIAEAA5AB0ACQAIAOn/FwBJACEABAAWAAYA1v8SAAUAOQAjAMb/BgDz//D/+/8nADgA4f+v/xYA+v+w/xcAGAC7//L/CgDD/+v/EQDx/9D/yf/e/y0A/P+z//z/CgDT/7b/+P8QAPD/DwAeANP/yv8OABkA/v/8/xAACQAjAPP/8f84ABsA5v8zABAA5P84ABoAHwA/ACAADAAfAC4AAwAtACYAIABJABEAMQDz/zgAPwABAC8AKAAAAM//EAAQAA0AAAC9/wEA7v/v/+P/9P/m/+H/GACc/73/2v9PAP3/af/o/wgA6f+q/6j/GAApAMH/sv8MAOH/wv/j/wsA1P/Q/0AAo//2/xMAFQD6/yIADgDy/wgAHwAGAAYAYgBUAMT/x/9mAFUAGQDz/yoADwAPAB8AdQAbAOz/TABPAPj/DwBZAIcAQQDo/ysAcAAjANH/MABxAO3/+P+UABgACwARACYACADs/9D/PgAfAMH/+P/d/woACQDD//f/2f/8/9L/AwD2/33/MQAqAFj/sP+CAMj/cv+0/1sABwBL/9z/KgCp/4P/0f8pAOH/tv9n/wcAKQB3/9H/6/8yAOr/gv8RALf//v8LAGAAiv+N/3sAhACd/1v/agCCAJ7/qv8mAHAAyP8wAFkAzP+//1IAUQAAAAgA2f+SACsAvP8mAG0AgACL/xAABgDd/yYAUgAzAN3/KADI/xkA1v/k/0QAHwCr/7z/FAAOAMP/mv/6/y0ASf+1/xMAagDP/67/of/x/9D/AQAAAOP/CwC///7/0v+X/0cARADv/57/YgBMAJX/5f9tAFQAxP81AFAAJADK/yAAlwBDAKH/dwBwAPb/sP9mAMAA7//v/2YAKAC7/x0AyQD+/wUAgQBBAJP/8f+MAPf/PgCg/w=="} (15:48:00.570) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.591) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.611) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "BwACAO//BwATAOH/z//2/w0AAQDJ/77/7v/6/9z/0P/h/wQA+v/M/8//+P/x/+f/5//u/9r/7f/z/+L/5v/z/+r/3//n/+T/6v/s/+j/8v8SAPj/6f/o/wUADAAAAPr/BAAUAAoA+/8EABwAIQAOAA8AEgAYACIAEwAbAC0AIwAiACoAHwAUAC4ALwAXACMAOAAxABMAFQAuACsAGwASACAAAgD4/xUAGgAOAPz/BwD9/xUAEgD4/+r/9v8IAAcA8//c/+r/CQD+/9r/4/8DAAUA3//X/+H/7//v/8//3P/p/+X/1v/c/+P/7//4/+3/4P/0//T/6f/w/wYAAgD5////AQD9/wYAHAAPAAIABAANAA8AGwAaABMAGgAdABUABAASACwAIwANAAgACwAWAA0ABQAWABUABAAEAA0ACAABAAAACgAEAAIA+/8AAP7/9v/4//X/9//v////3v/i//T/+v/l/+L/6P/p/+b/4f/c/9T/5f/s/9T/0P/i/97/3f/l/+v/1f/W/+D/7P/p/+D/4v/v//7/9//2//3/BwD//wIAEwATABAAFQAcAA0ACgAcAB0AGQAWACAAJAAcABYAHQAuACYADAAJACYANgAXAAMAGgAzABIA9/8RADQAHAD7//3/DAASAAAA+f8NABEA+f/1//7/+//w//v//P/y//H/6v/n/+P/6P/x//P/5v/Z/9X/0//a/+H/6//r/+n/5v/d/+n/7P/k/+j/9v/6//X/8v/6/wAA///4//7/DAATAAoA//8HAA8AGQALAAwAGwAcAA0ADAAfACMAHQAaACwAKQAiACQALQA0ACgAKAAjACUAJgAvACsAHwAgABkAGQAfABsACQAHABAADwAAAP7/BQAGAPX/7v/0//v//P/u/+v/6//f/9X/1//g/+P/3P/X/8n/0P/V/87/yf/S/9v/0f/A/8X/3P/i/9j/4f/m/9//1v/g/+n/7//w/+3/8v/7//7//v/7/wEACQAMAAkADwAXABUADgAPABcAHgAjACgAJwAnAC0ALwAxADEAKQAnADEALwAlABwAJQArACcADwAKABYAHgAUAP//AwACAAIA+P/v//X/9//y/+b/5v/m/+T/3P/e/+L/4//Y/87/0v/b/9r/1v/R/9j/1P/O/8j/zv/V/9b/3P/Z/9T/1v/b/+D/3P/b/+P/6//p/+X/6//+/wgABgD8/wEACgANAAsACwAXABsAGAASABoAJgAiABoAGwAoACwAJgAgACUAJgAjAB0AIgAnACQAHAAYAB8AIwAeABgADwAPABUADwAEAAEACAAIAP//8P/z//v//v/5/+7/6v/m/+r/6f/i/97/3v/i/+L/3f/X/9r/2//Y/9T/1f/W/9n/2f/Y/9r/4P/l/+D/3P/i//D/8v/s/+j/8P/9/wQA/f/6/wUAEAAMAAUACAAXACEAFgAJAAkAGgAjAB8AGQAbACMAIwAbABcAHAAjACIAGQAUABYAHAAZABMAEAATABEACAAEAAYACQAHAAIA/P/5//n/+f/3//X/9v/3//X/8//x//L/8v/1//f/+v/5//P/7//v//b/+//+//v/+f/5//v//f/+//3//P8AAAMABQAFAAYACAAIAAcACQAPABUAGAAVAA8ADQAOABUAGgAdABwAGQAWABQAFwAbAB0AGwAaABsAGgAVABEAFAAaACAAHgAXAA8ACwAPABIAEwARAA4ACwAJAAcAAwABAAEABQAGAAUAAwD///v/+P/4//r//f/8//n/9P/w//H/9f/5//n/9P/t/+r/6f/r//D/8v/v/+r/5v/m/+r/7//v/+7/7P/r/+n/6P/p/+v/7f/s/+n/5v/m/+X/5v/n/+r/6//p/+b/5v/q/+//8//0//L/8P/w//P/+f/9/////v/9//v//P8AAAMABgAJAAgACAAHAAkADQASABQAFAATABIAEgAUABYAFwAWABQAEwASABAADwANAA0ADAAMAAsACQAGAAMAAAD+//3//P/7//j/9f/z//H/8P/u/+3/6//p/+b/5P/j/+P/5P/k/+P/4v/g/+D/4f/i/+T/5v/m/+b/5//o/+r/7P/v//D/8f/z//X/+P/8//7/AAADAAQABwALAA=="} (15:48:00.632) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "EAATABQAFAAVABYAGQAdACAAIgAiACEAIAAhACMAJgAmACYAJAAiACEAIgAjACUAJQAiAB8AHAAaABkAGAAYABYAEgAPAAsACAAGAAQAAgAAAP7/+//3//X/8v/w/+7/7f/s/+r/6P/m/+X/5f/l/+T/5P/i/+L/4f/i/+P/4//k/+T/5f/l/+f/6v/s/+3/7v/v//D/8//1//j/+v/8//3///8AAAMABQAGAAgACgAMAA0ADwAQABEAEgATABQAFAAUABUAFAATABMAEgASABIAEQAQAA8ADQAMAAoACAAHAAYABAABAP///f/8//v/+v/4//f/9f/z//L/8f/w/+//7v/t/+z/6//q/+r/6f/q/+r/6f/o/+n/6f/p/+v/7P/u/+//8f/x//L/8//1//b/9//5//v//P/+/wAAAgADAAQABgAGAAgACgALAAwADQAOAA4ADgAPABAADwAQABEAEQAQABAAEAAQABAAEAARABEAEgARABAAEAAPAA4ADgAOAA4ADQAMAAoACgAJAAgABgAFAAQAAwABAAAA///+//z/+//6//n/9//2//X/9P/0//P/8v/y//L/8v/y//L/8//0//X/9v/3//j/+P/5//r//P/+////AAABAAMABAAFAAcACAAJAAsADAAMAA0ADwAPAA8ADwAQABAAEAARABAAEAAQAA8ADgANAAwADAALAAoACAAHAAYABAACAAEA///+//3/+//6//j/9//1//T/8v/w/+//7v/t/+v/6v/p/+f/5//m/+b/5v/m/+f/5//n/+b/5//n/+j/6f/q/+v/7P/t/+7/7//w//L/9P/3//n/+v/7//z//f/+////AQAFAAYACgALAAwAEAASABMAFgAYABoAGgAbABwAHAAdAB0AHQAeAB4AHQAdAB0AHQAcABsAGwAZABcAFgATABEADwAMAAkABwAFAAIA///9//r/9//1//L/8P/u/+z/6f/o/+b/5f/k/+P/4v/i/+L/4//k/+T/5f/n/+f/6P/q/+z/7v/w//P/9f/3//n//P///wMABQAHAAkADAAOABEAEwAVABcAGQAaABsAHQAdAB4AHwAfACAAIAAfAB8AHgAdABwAGgAZABgAFQAUABIADwANAAsACAAFAAMAAAD+//v/+f/2//T/8f/v/+3/7P/q/+j/5//l/+T/4//i/+H/4P/f/9//3f/d/9z/3P/b/9z/3P/c/93/3f/e/9//4P/i/+T/5f/n/+n/6//u//H/8//2//r//P///wIABQAIAAoADAAOAA8AEAARABEAEQAQABAAEAAPAA8ADgAOAA0ADAALAAoACgALAAsACgAKAAsADQAQAAsACQAKAAYABwAHAAMAAwAFAAMAAwAEAAMAAQD///z/+f/2//L/7//r/+j/5v/l/+X/5P/j/+P/4//m/+j/6//v//L/9P/2//j/+v/9/wAAAwAFAAcACwAQABUAGgAgACQAJwApACoAKwArACsAKwApACgAJgAiAB4AGgAXABUAFAAUABQAFAAVABQAFQAWABgAGgAeACEAJAAnACoAKwAsAC4ALwAvAC8ALAAoACIAGAANAAIA9v/o/9j/xf+u/5T/ev9g/0j/Nf8p/yP/H/8f/yT/K/86/0//bP+T/8b/BgBSAKYAAAFgAcABIQKAAtgCJANaA3QDbwNJAwEDmAIMAmEBjgCK/2D+E/2r+zz63Pic93T2WfVB9DfzcPJA8v7yAPVn+Or8NQKqB4EMYxAvE+sU1BUdFtcV6BQUE1cQ6QwDCQ8FfgGE/hH87/nO93b13vIg8GXtwupG6Bvmi+T/49nkg+d87AzzdfpSAmYJJw8UFDUY0hvsHgggxh72GpMVPBDrCpIGpQODAfX/mv5r/W38h/uA+h75VPc09dfySPCz7UnrH+ln51/mJeb65mnp/O2d9Mv8jQXSDdcUUhooHoggciEBIRwfdRs8FvAPPgk3A7H+APwo+3z7Bfzo+9D6zvh99lP0ZvJa8AHuKusY6GXlhuPx4irkVehb75/43QILDDwTYxjxG1EfeiKqJNokYyF4Gn0RPwgGAdz8l/tP/EP9ZP1h/Gv6i/hW95f2p/Vc84vveuoW5fHgq94w3o7fzePq6g=="} (15:48:00.652) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.672) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.694) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.713) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.735) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.755) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.776) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.797) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.816) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.837) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.858) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.879) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.899) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "/wAKARMBHQEnAS4BNgE8AUEBRQFGAUgBRQFAATkBLwEkARgBCgH6AOkA1gDDAK0AlwCAAGkAUgA7ACQADAD1/97/x/+0/6H/jv9+/2//YP9U/0f/Ov8t/yD/Ff8K///+9P7r/uP+3f7Y/tX+1P7V/tX+1/7Y/tr+3P7e/uP+5v7q/u/+9f79/gP/Cv8T/x3/JP8r/zL/OP8//0T/S/9U/1z/Zv9y/4D/jf+b/6v/u//M/93/7/8AABIAIwA1AEUAVQBkAG8AegCBAIgAjwCVAJsAngCjAKYApgCmAKUAowCeAJgAkQCKAIQAfAB2AG4AZQBeAFYATABGADoANAApACEAFgAOAAYA/f/6//T/8f/o/+H/3v/i/9j/xv/K/8b/tP+s/7H/ov+s/6j/p//F/7n/zf/R/+L/3f/a/+v/6P/e/+r/6v/m/93/7/8OAO7//P///1kAFgCaAF4AGwEFAQgAwwAeAd4AUwFlASsD5f/T//sEQADm/6r/O//3/tj+DgF4ApECbQFkAuYAGgDwACcCCgNoBG4ALf21/I36G/t+/ST+Dv2y/hkBfADcATgD7gIbAeUATP+i/er8d/63/eD9cAB1AOn/qQBpAXUA8wA0Ai4B9P9ZAvACnP+o/qIBR/9a/pD/HwCa/8z7z/9uAdL+o/8pA6wBXQBa/08BLADw/Qn/bQBg/7H8DACVAHz/2gEGAqQBwgAu/+/9Jf1R/nsBpP5C/0sBBQBZASQBtwAOA3P/yP9gAyoAoP5YAL4BPQBN/HIA8ADl/M3/1gHZ//z/7P+E/6IB8P1x/qz/pv/0/Cj/3gHI/W/+dQBv/ln96P4z/pD9zP7q/7/+aP5F/xEBQv/f/WQBogDm/DX+JAFx/XX7//+vBL38AQC8BK8CgAHgADIFeQIPAX//8wDkAnr+RQKa//gAiQCKAqX/QQFqBacBDf+WA6UCY/5rAhMDIv2g/+4DnP+N+yYC4wO3/tcAyQNVAaz/dgFYASYA7f99/jkAngNz/3L93AEiBJP9xf0JA1UAcvx5/twDkv9x+w4BWACa+3f8+gE8/y75A/+MAor+9PwQAqAB7Py7/woA6/+q/Cz9pP+b/uj8bP5CAbz+mP8NA68BTAJpAXwBCgHx/nsA+P1X///+of2tAnoG3/7dAC4EzAQ1AZAAMwQKBDMAeQHSAWMBowDt/4gFowBf/vYDgQTZAcH/LwMOAqIAPf42/qEAXAGg/Bb8RgPzAOH76/zSA5n+s/us/9r+nv44+wz/vPtz/qn+u/yx/mf9iP1T/jf/Sv0J/yoA1v8z/TH/xf9s/Q7+4QBJ/wH+YQFzAlz+fwFqAWn/RwHBAPcAff8ZA13+ywFQAGQBpAC6AU0EI/5uA2QBnwE4AGwAbQFt/rUA1gLn/sgCmv6VAaUDy/0BAnD8VgWu/YP9/gBY/wUCF/vt/+EBFv9X/ob8ygAAAKf80AEE+pYCF/9j/CP+Wv3IBDD7J/6c/SkAMP/9+iT9qf9B/igAgPwE/egCc/+9AZ/8u/1yBqX8JP7v/vz/kQMG/Cf+g/96BfT+/f2zAi4AZAMh/1kB3QKwA4j/UgJkAfYBnQKJ/o0EcgFjAcICkv+eAnAE+P/jAU0COgIZAucAWAC9AL8CuP/bAO3/VAEQAf/9BgIQ/7UAtwBz/2kAz/56Amn+E/4nAEABOf5C/ff9Yv7PAfT74fxk/0D/z/yQ/a/+hP1m/db+mv59/HL+av59/v/9Uv8z/0/+5/2MAML+k/6p/jEATf6Z/6L/f/6w/3r/VQGy/5IAZv88/n4BnAKw/GL9nAL5Akf+x/yCA24BLgA8AP7/hgI7AFsC7P/LAEUCkwGhAT/+VQPqAosAc/4wAo0D4P+hAE7/tAGpAbT+fgG4/xD/EwHpAL7+bP4ZAoX+Iv/S/8X+Gv+K/9n+sP8X/7f/2P4H/3sBnvwjAHcA2vwP/uL+VABGAH78y/0DAZUAE/zU/VIBcwGNAO78bP0/Ac0Bkf74/YAASgG1AWr99v7WAHQByf8rABsBYgAiA2P+VwAEA67/CwEuA5UARABDAdcDHADFAGIBOgOJAV/+gQGQA5f//f/RAlMDfgEXALUAtABlAl4AhgBSAMEA7gDtANX/K/69Ae8AkgADAQ=="} (15:48:00.920) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.941) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.962) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:00.983) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "UveX9hf2tPVt9Ur1XPV29bL1EfaY9jb3+fft+On56Pr3+wX9DP4X/y0AOgFSAn4DuAT0BSUHYQiVCZUKZQv6Cz8MOgzRCzELVApHCR0I1gZ2BQYEjwIkAbX/Uv4C/cb7pfqg+bv4+vdW9872bfYu9hv2L/Zm9tD2WvcH+MP4mPmC+m/7XfxT/VT+TP9DADsBPAI1Ay8EIgUfBhAH9we8CFsJzwn9CeMJjAkBCUYIagd1BnYFVwQrAwkC1wDF/7z+uv3h/BT8UvvX+n36MvoI+gL6E/pC+nn6v/oL+1n7r/sK/G382vxK/bP9Hv6L/vf+Yv/C/yMAgQDcAD8BngH9AVgCrwIAAz8DcwOTA6IDmwOGA1wDIgPcApACPQLhAYQBJQHGAGYABwCo/0v/+P6v/nb+S/4u/h7+Ff4Y/h7+K/46/lD+a/6L/rD+1v7+/ir/Wf+H/7j/6v8dAFQAhQC4AOcAEQE2AVABZQFzAXcBdQFsAVwBRgErAQ8B7gDKAKEAeABQACcAAwDe/8D/p/+S/4D/cf9j/1j/TP9A/zT/J/8g/xn/Fv8T/xL/E/8W/xz/I/8u/zz/Tv9h/3n/l/+3/9r//v8hAEMAYQB9AJEApQC1AMEAyADLAMwAywDGAL0AsgCjAJIAfwBpAFYAQAAvAB0ADAD+//D/5P/a/9H/yP+//7f/sf+s/6b/of+e/5r/mv+d/6H/rP+0/77/yP/T/93/5v/0//7/CgARAB0AIgAnACoAKQApABcAEwASAAoAFQAWABcAHQAkACQAKwAxADUAOAAzADUAJwAiABQACQD3/+b/5f/M/9f/tv+2/9X/tf/LAK7+jwGa/wACfwKv/Mn/KP7y/93/WgBEAOL+xf/N/3L/DQCQAAQAGADy/xMALgCQADAAkwDd/wEAPQCy/zwACwDbANb///9z/6v/7f/D/7H/5f80AHz/mwCQAPz/wAAOAKAAFv+XACIAIf+3AJX/mABa/6sAq/5JALT/AgBbAFj/lQDO/2QA0f/D/nABD/7kAQP+tf+/Afb70ANe/d4B6P66APL/qP8OAO3/dAA4/t0C//2FAMQArf61AXD+zwG2/n0B2gAP/nQBUv4cAfP+jAF7/VICOP3IAe7+Df+sAeX88gJh/bECB/6KATb/TQCt/+H/lgGa+3QFyfuxA7L91P9pAAoBu/shBtL5IgQp/OUEEvzcAl//RP8CAh39ZQX0+CQITPrxA8L/1f0vBeP5rwYJ/C0BwAFu/xX/zQTj+QAEWgDW/C8Dev60Aan9egF1ABH/ugBs/7YAG/9bAST/rgD2/2n/AAHU/PEFR/qyA7X8cAQF+2b+5Ahp82IMh/VLByn9YvyEBRn71QHX/er/ngEE/P4Do/3pApH6SASwALP4lAvF8kMMdfaEBDYB8faxDw7sWBN97q8PQPdxAJcH2POuDQTyqhVg5sIZWexiCWQBjveKEOztARVe6FMcYePbF6rvJQffBFrx6xAQ8VkLg/N/DNL4G/3uCibxtxG97MsOI/xw/NAC4P8oAOr8AQU++ZoGCflcA48AVPyoAjz9MAN5+ocETf2+AkD8wwKx/p3+sgES/xcAbwBV/vMCVPzPAX4Amv38Abz/e/4/Alv9dgOQ/8r75Qdi9sAHYPszAAUD+/yRAnL8sQWM+aYFmfnXB0z5+ASA/vn+HgPj+w8FWvxRAj/9DgTs/CsAlAF+/7gBYPtSCEz4RAQh/yT/kwGx/RwCTf9FAaz8HALZ/9j+UgHU/gAECfy+AtH/Pv6gAQL+fALR/Q4CT/3tAb/8gQM+/S8AdwIM/bkBKv4DAjsAKP4zAoH+eQER/6oAJAFI/gMEuP3NARD/CAESAMj+TgGQ/9H+OAES/TICv/1WAM8AL/5UAUr/hP+D/xb/5f41/7D/1v4F/uX/s//h/Qz/hf8MAFP9RwGH/vv/gf4LAdL/7/95AMgBAP+UAVT/6AHq/hABvgDUAIEAeAFgAkUBeQPQAf8DxwEGA4oCWgHQAl0C8wBYAgj+sP8C/mj9N/1O/eL9Av3u+WT8z/n7+Db28vf2+PXyNPiW9034wPkq+wX/iAA3CpEOxREOFtQYchiBE9wSwA5MCfQFVAJ7/0T9nPze/bP/KQGWBeYH0gi6Cg=="} (15:48:01.003) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.024) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.044) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.065) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.085) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "8P/3//z/AgAIAA4AEgAVABsAIQAnAC4ANgA9AEMASQBOAFMAVgBYAFkAWgBZAFgAVwBWAFUAUwBRAE0ASQBGAEIAPwA7ADcAMgAuACkAJQAgABwAGAAVABAADQAKAAYABAAAAP3/+v/3//X/8//x/+7/7f/r/+j/6f/o/+f/5v/n/+j/6f/p/+r/7f/w//P/9v/6//3/AAAEAAcACgAOABEAFAAXABoAHQAfACIAJAAmACcAKAAoACcAJgAmACUAJAAjACAAHQAcABkAFgATABAADAAJAAcAAwD///z/+f/0//D/7P/p/+b/4//f/9v/1//U/9H/z//L/8n/xv/E/8H/v/+8/7v/uf+4/7j/uP+3/7j/uP+5/7r/vP++/8D/wv/F/8f/yv/O/9H/1P/Y/9z/3//i/+b/6f/s/+//8v/2//r//P///wIABwAKAA0AEAATABYAGAAbAB4AIAAiACQAJgAoACoAKwAsACwALAAsACwAKgApACgAJwAmACQAJAAkACQAIwAiACEAIQAfAB4AHgAdABwAGwAaABgAFwAVABMAEQAQAA8ADgANAAoACQAGAAMAAgAAAP7//f/8//v/+v/4//j/+P/4//j/+f/6//v//P/8//3//v///wEAAwAFAAYACAAJAAoADAANAA4ADwAQABIAEgATABMAFAAUABQAEwASABAAEAAPAA0ACwAKAAgABQADAAEA/v/7//n/9v/z//D/7f/q/+f/5P/g/97/2//Y/9X/0v/Q/87/y//J/8b/w//C/8D/v/+9/7v/uv+5/7j/t/+4/7j/uf+6/7v/vf++/8H/xP/H/8r/zv/S/9X/2f/e/+L/5v/q/+7/8//3//r///8CAAcACgANABAAEwAWABkAHAAeAB8AIgAiACMAJAAkACQAJAAkACMAIwAhACEAIAAeAB0AGgAZABcAFQAUABEADwAOAAsACQAGAAIAAAD8//n/9v/z//D/7v/r/+f/5f/k/+P/4f/g/9//3//f/97/3v/e/9//4P/h/+L/4//l/+f/6P/q/+z/7//x//T/9//5//3/AQAFAAgADAAPABMAFwAbAB0AIQAkACcAKQArAC0ALgAvADAAMQAxADEAMAAvAC4ALQAsACsAKQAnACQAIQAeABwAGQAVABIADwAMAAgABAABAP7/+v/4//X/8f/u/+v/6f/m/+P/4P/e/9z/2v/Z/9j/1v/V/9X/1f/V/9X/1f/W/9b/2P/Z/9v/3f/f/+H/4//n/+n/6//v//L/9f/6//3/AgAFAAgADAAQABIAFQAaABwAHwAgACIAIwAkACYAJgAmACYAJQAjACMAIAAeABsAGQAWABMAEAANAAoABgADAAAA/f/6//f/9f/y//D/7v/s/+n/5//m/+T/4v/g/9//3f/c/9v/2v/a/9n/2f/Y/9j/2f/Z/9v/2//c/97/3//i/+P/5v/o/+r/6//u//D/8f/z//X/9//6//z//v8AAAIABQAHAAoADQAPABEAEwAUABYAGAAZABoAGwAbABwAHAAbABsAGgAaABkAGAAYABcAFgAUABIAEAANAAsACgAHAAQAAgD///z/+f/2//P/8P/t/+r/6f/m/+P/4P/d/9v/2f/Y/9f/1v/U/9T/0//R/9H/0v/T/9P/1P/V/9f/2P/a/9z/3v/g/+L/5P/n/+n/7f/v//L/9v/4//v//v8BAAQABgAJAAwADgAQABIAFAAUABYAFwAYABkAGgAbABoAGgAaABkAGQAYABYAFQATABIAEAAPAA0ACgAIAAUABAABAP///f/5//f/9P/x/+//7f/r/+n/5//l/+P/4v/h/+D/3//e/97/3//e/9//4P/g/+L/4//l/+j/6f/s/+7/8f/z//b/+P/6//7/AQAFAAgACwAPABIAFQAYABsAHgAhACQAJwAqAC0AMAAyADQANgA3ADcAOQA5ADoAOgA6ADoAOQA5ADgANwA2ADUANAAzADIAMAAtACsAKQAnACQAIgAfAB0AGgAXABUAEgAPAAwACgAHAAQAAQD+//v/+P/2//P/8f/u/+z/6f/n/+X/5P/i/+D/3//e/93/3P/b/9v/2//a/9v/3P/e/9//4P/i/+T/5v/o/+r/7f/w/w=="} (15:48:01.106) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "8v/1//j/+//9/wAAAgAEAAYACAAJAAsADAANAA4ADgANAA0ADQAMAAwACgAJAAcABgAEAAEA///8//n/9v/z/+//6//n/+P/3v/a/9f/0//P/8r/xv/C/77/uf+2/7P/rv+r/6n/pv+j/6H/n/+e/5z/mv+a/5r/mv+a/5r/nP+d/6D/pP+n/6v/r/+z/7j/vf/C/8j/zf/T/9n/3//l/+r/7//0//r//v8CAAYACgAOABEAFAAXABkAGwAeACAAIAAhACIAIwAjACIAIgAiACEAHwAeAB0AHAAbABkAFwAVABIAEAAOAAwACQAHAAUAAgAAAP7//P/6//f/9f/z//H/7//t/+v/6v/o/+f/5v/k/+P/4//i/+H/4v/i/+H/4v/i/+P/4//k/+X/5v/n/+n/6//t//D/8v/1//j/+//+/wEABQAIAAwAEAATABYAGgAdACEAJAAmACkAKwAtAC8AMAAxADIAMwAzADMAMwAzADMAMgAwAC8ALgArACkAJwAlACMAIAAdABoAFwAUABEADgAKAAYAAwABAP3/+v/3//T/8f/u/+v/6f/m/+X/4//i/+D/3//e/9v/2//a/9r/2v/b/9z/3P/d/97/3//g/+L/5P/n/+j/6v/r/+3/8P/y//T/9//6//z//v8AAAIABQAGAAkACwAMAA4ADwAQABEAEwAUABUAFgAWABYAFgAWABUAFQAVABQAEwASABAADgANAAsACQAIAAUAAwACAAAA/f/8//r/+P/2//T/8v/x/+//7f/s/+r/6f/o/+j/5//n/+f/5//o/+j/6f/q/+v/6//s/+3/7v/v//H/8v/z//T/9f/3//n/+//8//7/AQADAAUACAAKAA0ADwASABQAFQAXABkAGwAcAB0AHgAgACAAIQAhACIAIgAiACIAIgAhACEAIAAfAB4AHQAcABsAGQAYABYAFQATABEAEAAOAA0ACwAIAAYABAACAAAA///9//v/+f/3//X/9P/y//D/7v/t/+v/6v/o/+f/5v/l/+P/4v/i/+H/4f/h/+L/4v/j/+T/5v/n/+n/6v/s/+7/8P/y//T/9v/4//n/+v/7//3//v8AAAEAAgADAAQABAAGAAYABQAGAAYABgAFAAYABQAFAAQAAwADAAIAAQAAAP///f/8//r/+f/4//f/9f/0//P/8v/x//D/7v/t/+z/6//q/+n/6P/o/+f/5v/m/+X/5P/k/+T/5f/l/+T/5f/l/+X/5v/m/+f/6P/p/+r/6v/r/+z/7f/v//H/8//0//T/9v/1//b/9//2//b/9//4//b/9//2//j/+P/5//v//f/8//z//P/5//v//f/8//v/9//2//T/8//x/+//7//t/+z/6//t/+3/7P/s/+z/7f/s/+z/6//r/+v/6//r/+v/6v/p/+n/6f/p/+r/6//s/+3/7v/w//L/9P/0//X/9//5//z//v8AAAMABQAHAAkADAAPABEAEwAUABUAFwAYABgAGQAZABoAGgAZABkAGAAYABcAFgAVABUAEwASABEAEAAPAA4ACwAKAAkABQADAAIAAAD///3/+//5//b/9P/y//D/7v/s/+r/6P/o/+f/5f/k/+L/4P/g/+D/4P/g/+L/4v/h/+D/4v/k/+b/5v/m/+j/6f/p/+v/7f/w//L/8//0//f/+v/6//v//f8AAAMABwAKAA4AEAASABUAGAAbABwAHwAjACYAKQArACwALgAyADEALwAxADUAMwAxADEANwA4ADMALQAsAC8AMQAvAC4ALQArACYAIQAgACAAHwAcABoAGQAZABgAFgAVABUAFAARABAADwAPAA4ACgAHAAgABwAFAP7/+P/4//r/+f/z//D/8//2//X/8f/y//f/9//y//D/8//7/wAAAgABAAIABgAIAAsAEAATABIAFQAXABkAGwAhACUAIwAfACEAKAAsACwAKgAuADEAKwAlACgAMwA2AC0AJgAnACsAKAAfABsAHwAeABUADQAPABQADwAFAP7//v/8//X/8f/x//L/7f/m/+H/4f/g/9n/0f/Q/9P/0v/N/8r/zf/N/8j/xP/G/8v/zP/L/8v/z//U/9X/1v/a/9v/2v/b/9//5P/p/+3/7v/v/w=="} (15:48:01.126) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "8P/1//r///8BAAMABwAKAAoADAARABkAHAAbABwAHwAkACMAIQAiACcAJwAjACAAIgAnACYAIgAdABsAGQAUABEAEwAXABcAEQAJAAQAAwABAP7/+//6//f/8f/r/+n/7P/s/+n/4v/e/9z/2v/Y/9b/1//W/9P/z//O/8//zf/K/8n/yf/K/8f/x//J/8r/yv/G/8j/yv/J/8j/yf/N/8//zf/O/9H/1f/V/9P/1f/b/97/3P/b/+L/6P/o/+X/4//m/+3/8//z//L/9f/4//j/9//4//3/AAACAP////8DAAgACQAIAAgACQAKAAwADAANABIAFAALAAUADAAYABkAEgAKAA4AHQAQAPz/AgAkADAADgDr//j/HwAqABQAAQALABYACwAAAAoAHwAdAAQA+f8NAB4AEAD6/wIAFgAYAAcABQATABUABwADABIAHgAYAAoACQAZACEAHQAZAB4AHgAhAB0AHAAiACsALwAiABcAHAArAC8AKQAiACEAJgAiAB4AIgAmACcAHAAUABkAIQAfABEADQAWABoAEAAHAAoADwAMAAMAAwAHAAMA+v/5//z//v/6//L/8v/3//n/8//x//T/9v/y//D/8v/4//b/8P/w//L/9f/1//X/9//5//n/9v/4//3///////7///8CAAMAAgADAAQABgAHAAYACAAJAAoACgAJAAoADQANAA0ADQAOAA8AEQAQAA8AEAAQABAADwAOAA8ADwALAAkACQAJAAoACAAHAAYABAADAAIAAgABAP/////+//z/+v/6//r/+P/2//X/9v/2//T/8//y//H/8P/u/+//8P/w/+7/7f/s/+3/7//v/+//7v/v//D/8P/x//L/9f/1//X/9v/3//f/+P/6//v/+//8//3/AAAAAAAAAAAAAAEAAgAEAAUABgAGAAQABAAEAAcACAAIAAcABgAIAAkACgAKAAoACAAIAAcACQAKAAsADAAHAAUACAAJAAkACQAHAAYACAAEAAQACAAGAAsACAAGAAYACQALAAIAAwAIAA8ADAABAAAABAAGAAgAEgANAP//9/8CAA4ADAAAAPb/BgATAAkA/P8DABQADADp/+T/GAA9ABYA5P/Y/+T/CwALAA8AJgAKAOv/zf/W//P/JAAqAOb/wv/X/xUAEADp/+j/BAAIAO//uP+M/+//TQAlAMz/sP/q/wMAzf/Q/yMAMQDK/4n/5/8kAPj/vP/R/0EAQACv/7H/3f8JAA4A0v8mAAEA/f/t/wUAJgDQ/7f///9oAPz/cP+0/woAPAD0/wkANACC/zT/lf/EACoBKgAh//r+EADfAK4ASgBM/z//aACpANH/gP/c/7IATgDO/jj/wwBTAUQAXv9o/ygAlgCjAMr/Jf87/zcACgFuAKD/Tf/r/xUAx/+S/y8AUQCxAD8AAQCFAKcAMQD/////wQAXAIX/LQBzAJQAR/+G/rj+yv/2AKAA9P6O/n3/FQDyAOEALwBP/7j/pwEnA7oCawA8/6j+Lv14/ssANANLAuj+gPvl+/j/SgEJAcr9Zf0l/9wAWwIdAngBHP+I/cT/kgEPA7cC5v/Y/ij94f6KAJH/xf5J/g//FAAHAK0Al/5DAEABswDHAPP+2v8jAJIAKADt/sv+bv8oAaYAZv+b/uT/cAGX/on94v6RAPQBFf+I/8kAFwB0/Xj+BAIbAdv+IP/cAeoBmv41/aQAdwIPAnT9+/6TAJ0ATgLVAM8Bc/5a++b8fQAOArYA3AG4/9f8HgAiAGsAAwB2AJkACAAuAAABugI5AYP/Lv+0/34AQgGXAYn/CP4H/6b/qwBT/k7+PgF2ATb+ZfzTAM4CwP/n/aX+yQRdBqkCOP0r+o7+3gHOBDMC+v4L/h/9ef6tAXL/uP0w/Cr+CwTdAjIAAPwh/w0CtQC4AtX/cwEAAX0BQgKw/5QBQP3s+Rn/XQVgBgH9Vfhs+5kAZwItANMB0wA1/oP98f6GBPkEKQJs/g3+F//+AuoCowDy/Ln9BAGGAYAAOf5I/h//+f8FA5UCHgBJ/Dj/nQK0/xX9F/xq/+4CmwA+/9YATQJz/Yj9lP5KARACqQADAvgBgQCx+xD+PgS1AkcA9vw4/Gz/lgFYAiT/M//Y/w=="} (15:48:01.147) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.167) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.221) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.222) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.230) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.250) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.270) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.291) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.313) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.333) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.354) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.375) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.395) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.416) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "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"} (15:48:01.436) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "OAA6ADwAPgA+AD4APwA/AD8APQA7ADgANQAzAC8ALAAoACQAIAAbABgAFAAQAAwACAADAP//+v/2//L/7f/p/+T/4P/c/9f/1P/Q/8z/yf/G/8P/wP+9/7v/uP+3/7b/tv+1/7T/tP+0/7X/tv+4/7v/vv/B/8P/xv/J/8z/0P/U/9j/2//g/+L/5v/p/+3/8f/0//j/+//+/wEABAAGAAgACgAMAA0ADgAPABAAEQARABEAEgARABEAEAAPAA4ADQALAAkACAAFAAQAAgD///3/+//4//b/9P/x/+//7f/q/+n/5//m/+T/4//h/+H/4P/f/9//3//f/9//3//g/+D/4v/j/+X/5//p/+v/7v/w//L/9f/3//v//v8BAAUACQALAA4AEQAUABcAGwAdACAAIgAlACcAKQArAC0ALgAvADEAMgAzADMANAA0ADQAMwAyADEAMAAwAC8ALQAqACgAJgAkACEAHwAcABkAFgATABAADgAMAAkABwAEAAIAAAD9//v/+P/1//P/8f/v/+3/6//o/+f/5v/j/+L/4f/g/9//3v/e/97/3f/d/97/3v/e/9//3//g/+H/4v/j/+T/5f/n/+j/6f/r/+z/7f/v//D/8v/z//T/9f/1//b/9//3//j/+P/5//j/+P/4//f/9v/1//P/8v/x/+7/7P/q/+j/5f/j/+D/3f/a/9j/1f/S/8//y//J/8f/xP/D/8H/wP++/73/vP+8/7v/u/+8/73/vv+//8D/wf/D/8X/x//J/8z/z//S/9b/2f/b/9//4//n/+v/7//z//f/+//+/wIABgAKAA4AEgAVABkAHQAhACQAKAArAC8AMgA1ADgAOgA7ADwAPAA8ADsAOwA6ADkAOAA2ADUANAAxAC8ALgAsACoAJgAkACEAHwAcABsAGQAWABQAEgAPAA0ACwAJAAYABAADAAEA///+//3//P/7//r/+f/3//f/9//2//X/9v/1//X/9v/3//f/+P/5//r/+//9//7///8BAAIABAAGAAgACQALAA0ADgAOAA8AEAAQABEAEQARABEAEQAQABAADwANAAwACgAJAAcABQADAAEA///8//n/9//0//D/7f/q/+f/5P/g/93/2f/U/9D/zP/J/8b/w//A/73/uv+3/7T/sf+v/63/rf+s/6z/rP+t/63/r/+x/7P/tv+5/7z/vv/B/8X/x//M/9D/1f/Z/97/4v/m/+v/8P/0//n//v8DAAgADQASABYAGwAeACIAJgAqAC0AMAAyADQANgA3ADYANwA3ADUANAAyADAALgAsACoAKAAmACMAIQAeABwAGgAXABQAEQAPAAwACQAGAAMAAAD+//v/+P/2//P/8P/u/+3/6//q/+j/5//m/+X/5f/l/+X/5//o/+n/6v/r/+3/7v/w//L/9P/2//j/+v/8//7/AAACAAQABwAJAAwADgAPABEAEwAUABcAGQAaABsAHAAdAB4AHgAfACAAIAAfAB8AHgAdABsAGQAXABQAEgAQAA0ACgAHAAUAAQD+//z/+P/1//L/7//s/+j/5f/j/+D/3f/a/9f/1P/S/9D/zv/M/8n/yP/G/8b/xf/F/8X/xf/G/8f/yP/J/8v/zP/N/9D/0v/V/9f/2v/c/9//4v/l/+j/6//u//H/9P/3//r//P///wEABAAGAAkADAAOABEAEgAUABYAFwAZABoAGwAdAB4AHgAeAB8AHwAdABwAHAAbABoAGQAXABUAEwASABAADQAMAAoACAAGAAQAAgD///7/+//5//j/9v/0//P/8f/v/+7/7f/s/+z/6//r/+r/6//r/+3/7v/v//H/8//0//b/+P/5//v//f///wIABAAHAAsADQAQABMAFgAZABwAHgAhACMAJQAnACkAKwAsAC0ALwAvAC8ALwAuAC0ALAArACkAJwAlACMAIQAfABwAGQAWABQAEQAOAAsACAAEAAEA/f/5//b/8v/v/+v/5//j/+D/3P/Z/9X/0f/O/8v/yf/G/8P/wf++/73/vP+7/7r/uv+6/7r/uv+6/7v/vP++/8D/wv/E/8b/yf/L/87/0f/U/9f/2v/d/+D/4//m/+n/7P/v//L/9f/3//n/+//+////AgAEAA=="} (15:48:01.457) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "BgAHAAgACQAKAAoACgALAAoACgAKAAoACQAIAAcABgAFAAMAAgABAP///v/7//n/9//1//T/8f/w/+7/7P/q/+n/5//l/+T/4//i/+L/4f/g/+D/3//g/+D/4f/i/+L/5P/l/+b/6P/q/+z/7//z//X/9//5//z///8AAAMABQAIAAsADQAQABMAFgAYABsAHAAfACEAIwAlACYAJwAoACkAKQApACkAKQApACoAKgApACgAJwAlACQAIgAgAB8AHQAbABgAFgATABEADwAMAAoACAAGAAMAAAD9//v/+P/2//P/8P/u/+z/6v/o/+b/5P/i/+H/4P/e/93/3P/c/9v/2//c/97/3//f/+H/5P/m/+n/6//u//D/8//2//n/+//9/wEABAAHAAoADQARABMAFQAYABsAHQAgACIAIwAkACQAJQAlACQAJAAjACIAIAAfAB0AGwAZABYAEwAQAA0ACgAGAAIA///7//b/8v/v/+v/6P/k/+D/3P/Z/9T/0f/N/8n/xf/D/7//vP+5/7b/s/+x/67/rf+s/6z/rP+r/6z/rP+u/7D/s/+2/7r/vf/B/8X/yv/O/9L/1//b/+D/5f/o/+3/8v/4//3/AQAFAAkADQARABQAFwAaABwAHgAfACIAIwAkACQAJQAmACYAJQAlACQAIwAiAB8AHgAcABoAGQAXABUAEwARAA4ADAAJAAcABAACAP///f/6//f/9f/y/+//7P/p/+j/5v/l/+P/4v/h/+D/4P/f/9//4P/h/+L/4//k/+b/5//p/+v/7f/v//L/9f/3//r//f8AAAMABgAJAAwAEAATABYAGgAdACEAJAAoACsALgAwADMANQA2ADgAOQA7ADwAPQA9AD0APQA8ADsAOQA3ADYAMwAwAC0AKgAoACUAIQAeABsAFwAUABAADQAJAAYAAwD///z/+f/1//H/7v/q/+j/5P/i/+D/3v/b/9n/1//U/9L/0f/Q/8//zv/N/8z/zP/M/8z/zP/N/83/zv/Q/9L/0//W/9j/2v/d/9//4v/m/+n/6//t//D/8v/1//j/+v/8//7/AAADAAUABgAHAAgACQAKAAsACgALAAoACgAIAAcABgAEAAMAAQD///3/+//5//b/9P/z//D/7v/s/+v/6P/l/+P/4P/e/9v/2P/V/9T/0v/Q/87/zP/K/8n/yP/H/8f/x//I/8j/yf/L/8z/zf/P/9L/1P/X/9n/2//d/9//4f/j/+X/6P/r/+3/7//y//b/+P/7//7/AAADAAYACAAJAAwADQAPABAAEQATABQAFQAVABYAFgAWABcAFwAWABUAFQAUABMAEgASABEAEAAOAA0ADAAKAAgABwAFAAMAAgAAAP7//f/7//r/+P/2//X/9P/y//D/7//u/+3/7P/r/+v/6v/q/+n/6f/p/+r/6//s/+3/7v/v//H/8//0//b/+P/6//z//v8AAAIABAAGAAgACgANAA8AEQATABQAFgAYABkAGgAbABoAGwAaABoAGQAZABgAGAAWABUAFAATABIAEAAOAAwACwAJAAgABQAEAAIAAQD///3/+//5//f/9v/0//P/8v/w/+//7f/s/+v/6v/q/+n/6P/o/+j/5//n/+f/5v/n/+f/6P/o/+n/6v/r/+z/7f/w//L/9P/2//f/+v/8////AgAEAAcACgAMAA8AEgATABUAFgAYABkAGwAcAB0AHgAeAB4AHgAfAB8AHwAfAB4AHgAdABwAGwAaABkAGAAWABUAFAATABEADwANAAsACQAHAAUAAwABAAAA/v/8//r/+P/3//X/9P/y//H/8P/v/+//7v/t/+3/7f/t/+7/7v/v/+//8P/x//L/8//1//b/+P/5//v//P/+/wEAAgADAAUABwAJAAoADQAPABAAEgATABQAFQAWABcAGAAZABoAGgAbABsAGgAaABkAGAAXABYAFAATABEADwAMAAoACQAHAAUAAwABAP///f/7//n/9//0//L/8P/u/+z/6v/p/+j/5v/j/+L/4f/g/9//3f/c/9v/2v/a/9n/2v/a/9r/2//c/9z/3v/e/+D/4v/k/+b/6P/q/+z/7//x//T/9v/4//v//f///wIABAAGAAcACgAKAA=="} (15:48:01.477) +{"event": "reverse-media", "callId": "0.1.0.18.0_CallID", "streamId": "C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav", "chunk": 1, "chunk_durn_ms": 20, "mediaFormat": {"encoding": "PCM", "sampleRate": 8000, "channels": 1}, "payload": "CwAMAAwADQAMAAwACwALAAkACAAHAAYABQAEAAIAAQD///7//P/7//n/9//2//P/8f/v/+z/6v/n/+b/5P/i/+H/4v/j/+L/4P/g/+D/4P/f/+D/4P/g/+D/4P/h/+H/4v/j/+T/5v/n/+n/6//s/+7/8P/0//f/+f/8//7/AAADAAYACQALAA0AEQATABUAFwAaABwAHQAfACAAIQAhACIAIgAhACEAIAAfAB4AHQAcABoAGQAWABUAEgAQAA0ACgAHAAQAAQD+//z/+P/1//L/7//s/+n/5//k/+H/3//d/9v/2v/Z/9j/1//X/9f/1//X/9f/2f/Z/9r/2//d/9//4P/i/+P/5f/n/+n/6//t/+7/8P/y//T/9f/2//j/+f/6//r/+//7//v/+//6//r/+P/4//b/9P/z//H/7//s/+r/6P/m/+T/4f/f/93/2//Y/9b/1P/S/9D/zf/M/8r/yP/H/8b/xv/F/8T/xP/E/8X/xv/H/8f/yP/J/8r/yv/M/87/z//Q/9L/0//V/9b/2P/Y/9n/2//b/9z/3v/i/+b/7P/v//T/9//7//3/AAD//wUA//8FABIAEAAWABIADwAQABIAEAAOAA8ADwAKAAsADQAOAAwADQANAAwACgAIAAcABwAJAAkACQAKAAoACQAJAAsACwALAAoACgAIAAcABwAIAAcABgAGAAUABQAFAAUABQAEAAQAAwADAAMAAwAEAAMAAwACAAIAAgACAAIAAgACAAIAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAABAAMAAgADAAMAAgACAAIAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAgABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAEAAAAAAAAA///+//z//P/8//z//P/7////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="} (15:48:01.498) diff --git a/audio/sent-to-BOT.txt b/audio/sent-to-BOT.txt new file mode 100644 index 0000000000000000000000000000000000000000..a977d3b0e0c6723fec11ada2520c3475ae773ed2 --- /dev/null +++ b/audio/sent-to-BOT.txt @@ -0,0 +1,18 @@ +{"timestamp":"2025-01-17 11:07:06","streamId":"C:\\record\\0.1.0.18.0_CallID_9911011200_051025154759.wav","callerId":"9911011200","channelId":"0.1.0.18.0","event":"answer","callDirection":"incoming","did":"0120111222","callId":"0.1.0.18.0_CallID","cid":"CallID","extraParams":""} (15:47:59.580) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"sAIwAzACcAPQAmAFIAVwAjgAcPzQ/BACwAuAEEAJwPWA4ADbgOWQ/YAQgBaAEEAKwArADEAJ0PyA7IDlgOpA9+AFQAzAC+AF6AFYAJD9IPvA90D1wPag+OD7kP3o/tADwAqAEMAN4AVw/cD3YPnw/BADIAZgBWAGoAbACsAKoAUQ/EDzQPTg+vACoAZgBagBkAPADoAXgBNw/QDfAMsAz4DmYAWAGoAdgBqAGIAXgBPgBIDvAN0A2YDkYPhACYAQgBHADEAKIAWw/EDzgOqA6YDvwPdYAWAFQAlADIAQwA/gByj/QPbA9GD4qP5gBSAGwAnAC8AKQAnQA7D9QPbA88D2oPvYAfAD4AXgBJADQAjADUAOsAOA7ADVANOA43j/gBgAIYAbgBWAE8AOIARA84DhANuA5MD3wAmAEoASQA4=","callId":"0.1.0.18.0","extraParams":""} (15:48:09.927) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"gBGAEoASgBeAGIAUwAwYAGD4wPWA74DrgO3A9KD7iACwA2AHQAvACfACUPxg+qD6YPog+7D9SAFgBkALwAzACsAIYAXYAdD94Plg+CD5YPmg+KD5UP0oAbACWAHQAmAFoARgBNACiACYANgAMALQA+D6wPBA9uD5wPKA78DxYPlgBcAKwAqAEYAZgBiAEcAIEAIY/sD3gO6A6IDswPPA92D7UALACcANwAmwAgj/4PtA98D0QPZg+pgA4AVACkAPgBDADUAK4AX4AJD8YPnA9sD0QPRA9iD6WP5IAPACwAhACSAH4AUQA4gAKP9w/VgACP/A8cDwYPvA98DxwPNA9Qj+oAdgBkAKgBaAGIAVgBJADGAH8AJA94DtgOuA7IDuwPHA9sj/QAhACkAIoAegBWgAoPvg+aD6cPzI/pgBYAY=","callId":"0.1.0.18.0","extraParams":""} (15:48:10.020) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"QPWg+KD7iADACcANwA1ADkANQAygB2gAYPtg+cD1wPFA8cDzwPWg+TD8eP/QA6AGoAfgB8AIwAggByAF4AQgBFACGAHIAPj/eP4o/jD9EP3Y/qj/GP/o/ggBWACI/xgAWAC4AKj/SP/o/9gAiACIAXADEP2w/XgAoPog+ED2wPeg+SD7CP7oAOAHwAjACcAKwAnACWAF2ACQ/WD7YPjA9ED1QPbA9+D5oPvQ/QgB8ANgBSAGYAfgB+AG4AUgBZAD8AKoAQgACAAI//D8kP1o/kj++P7Y/zgA6ADoAEgAeAD4AOj/qP74/ij/2P5o/kj/IPuw/SgAIPtg+WD5UPwQ/DD88P0Y/+AF4AQgBCAHoAegB5ADyACY/7j+oPvA92D5YPog+iD7cPz4/vgA0ALQA+AEoAagBiAEoAUgBjAD8AI=","callId":"0.1.0.18.0","extraParams":""} (15:48:10.061) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"SP6Y/5gBUANgBGAFIAYgBmAFIARwAmgA8P3g+6D6oPkg+eD5oPtw/Wj/iAHQA+AFYAagBiAGoAWgBDAC2ACoANj/aP6I/tj/qP+Y/zgAmACIAEgA2P9I/xj/mP6Q/Tj+eP6I/tj+KP8o/wj/SADI/xD9cP3Y/lD94PtQ/JD9GP7o/vj+yP/4AbACEANgBGAFYAUgBWAE8AJoASj/0Pyg++D5oPlg+lD80P1YALADoAWgBiAH4AZgBSAEOAHo/vD9kPyg++D7sP1I/gj/aABIAZgBeAFIAagAKACI/3j+iP7Y/pj+2P5I/4j+SP5Y/nj+MP1Q/Vj+KP9YAMgB8AKQAyAEkAOQAugByACo/wj/8P2Q/fD90P0I/qj+OP+I/gj/WP8I/1j/2P9IAKgAiAHIAfgBEAOQAxAD0AKQAogBiAA=","callId":"0.1.0.18.0","extraParams":""} (15:48:10.103) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"6P6I/0gAKAGYAdgBWAE4AGj/6P5o/ij+qP4IADgBUAJwApgBGADw/fD8UP0I/7gAqAG4AXgAmP9Y/4gAiAFoASgAiP7Q/Tj+mP/4AIgBSAFoAPj/eP8Y/zj/2P6I/hj/qABQAjADEAMIARj/OP6w/XD90P1I/xgB0ALQAxADWAFI/uD7oPtQ/RgAyAFwAlgB6P+I/5gA4ATAC6AGwPWA5YDlwPZADYAZgBSgBwgAqABgBdACQPeA7IDrwPfgBsAOwAygBnACmAFYAYj+YPlA9cD14Pu4AeAF4AbgBWAEMAL4//D8oPrg+SD7KP7oAeAE4AVgBHACaAAY/rD90P0I/jj+6P5wAmAFIAUoAVD8oPow/Dj/2ABoADj/CP8oAdADQArADyAFQPCA4IDkYPnADIAVgBBACKAFYAfgB8j+wPA=","callId":"0.1.0.18.0","extraParams":""} (15:48:11.922) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"QPeoAcAKgBKAGYAdgB+AHIAWgBDACbACcPxg+WD4IPgg+KD44PhA98D0QPPA8kDzwPTA9iD4oPmg+3D9KP9oAWAEYAfAC8APgBKAFIAUgBOAEEAM4AcQA8j/WP4w/eD7oPrA9kDwgOuA6oDqgOyA7YDvwPKg+DgAoAfADYASgBWAF4AVgBLADKAHyAEg+8D2wPTA9ED2wPeg+aD6oPrg+qD7cPwQ/RD9MPzg+9D88P2o/7gB8ANgBkAJQAzADUAPwA/ADsAMQAogB+AE8ANQA6gB6P8g+8DygOuA6IDogOqA7YDtgO/A82D6yAFACUAPgBKAFIAVgBOAEMAMQAmgBHj+oPhA9EDyQPLA80D1QPeg+KD6MP24/2gBuABY/nD84Psw/LD9uP+YAfAD4AZACUALQAxADcAMwAtACkAJwAg=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.020) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"wA2gB+AGwAhACkALwA1AD0AOwA1ADcAOgBWAHIAYoAWA6gDZANMA38DykAMgB9gBkP0wA4ARgB0AIYAYMANA8YDogOpA9TgAIARoAWD7IPlw/cj+wPSA5gDZANUA2YDjYPhACYASgBWAEkAOQAvAC0AMQAngBUAIgBCAGIAcgBqAFYAXgBuAF3ADgOUAzQDFAM+A6Hj/wApACWAHQAqAFIAagBfgB8DzgOiA6ED0cANACsAIiAF4/iAEQAn4/4DqANcAzQDTgOTg+cALgBKAEkAPQA5ADoAQgBDAC/ADKAGgB4ASgBiAGIAQwAzAD4AQoASA7QDXAM0A0YDjQPcgBcAIwAhADYAXgB+AHEAOoPiA6oDowPIQA0ANwAxgBbD88P1ACEALIPkA2wDHAMcA2cDxQAqAFoAUwA/ADEAPgBI=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.100) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"gBuAG4AZgBeAFYAUgBWAFEANqP6A74DmgOeA7UDxQPGA7sDyUPwgB0ALoAcgBKAGwA6AFMAP4AUo/rD8qAHgBaAG8AKg+oDvgOaA5YDrQPCA7oDpgOiA7uD4KAFQAzAC8ALACIAQgBSAFYAUgBSAFoAWgBaAFIARwAlY/8D3wPRA9sD2QPTA8cDxQPYw/WgBOACw/BD9MAJACkANwAnQAgj+WP/QAvADmAGQ/aD4QPXA88DzwPNA8YDtgOyA7sDyQPdg+SD6EP0QA0AKQA7AD4ARgBSAGIAZgBeAFYARwAtgBBD9IPhA9sD1QPRA8kDxQPOg+ND80P0w/bj+UAPACUAMwAqgBtAD0AMgBGgBUP3g+ED0QPCA7oDtgOyA64DpgOuA7sDxQPZg+eD7uABgB0AOgBGAE4AXgBqAHYAegB0=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.140) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"gBmAE0ALoARI/+D7IPnA9UDzwPFA80D3IPoQ/Lj+OACwAuAGQAnACMAIwAjACKAHYAQoAGD7QPWA74DqgOeA5oDkgOKA5IDngOxA8sD2IPpI/yAFQAvAD4ASgBSAGIAcgB8AIYAfgBqAFEANYAeQAsj+oPrA9kD0wPPA9SD4YPkg+9D8CP4oASAEYATwA9ADMAOQAxADCAFw/SD4QPKA7YDqgOmA54DlgOaA6IDrQPDA9WD6KADgBUALwA+AEoAUgBaAGYAdgB8AIYAdgBdAD0AIeAHg+8D2QPKA7oDtgO9A8kD2YPoI/tgA4ARACMAKwAvACsAIoAbQAzgBCP7g+kD3wPKA7oDrgOiA54DngOeA6IDqgO7A8sD3EP0wA0AJwA6AEoAVgBaAGIAagBuAG4AagBaAEUAL4AX4/yD7QPc=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.164) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"cAKgBsAKwA6AEoAWgBmAGYAYgBaAFIAQQAxACHADmP/Q/KD64PjA90D3wPfg+CD6YPsQ/dD9OP74/8gAOAD4/3j/kP0w/OD6oPjA9sD1wPTA80DzQPNA88DzQPTA9cD3IPmg+6j+eAEgBcAIwAvADoARgBSAFIATgBKAEEANQApgB5ADKAFI/5D9UPyg+yD74Ptw/Qj+mP5o/zj/aP84AOgAWABo/6j+MP2g+2D6IPnA9kD1wPRA88DywPLA8kDzwPPA9MD2oPig+pD9KAAQA6AGwAlADEAPgBGAE4ATgBOAE4AQwA1AC+AH4ATQAqgAeP7w/KD7oPtQ/HD80PyQ/bD9OP64/wgBKAF4ABgA6P5w/RD8YPrA98D1wPTA8sDxwPBA8MDwQPFA8kD0wPag+RD9KACwA+AHQAtADoARgBQ=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.239) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"gBWAFoAWgBSAEoAQwAxACaAF6AGo/uD7IPlA98D1wPVA9sD2IPig+aD6cPzY/rgAqAG4AcgB+ACo//D94Psg+UD2QPVA80DxQPBA8MDwwPFA80D2oPgQ/Kj/8ALgBkALwA6AEYAUgBeAGIAYgBeAFYATQA9ACyAHsAKo/iD7IPhA9UD0QPNA9ED1wPag+GD6kPyo/sgAUALwAvACsAKYARgA0P2g++D4wPbA9MDyQPFA8MDwQPFA80D1wPeg+lj+mAFgBcAJQA2AEIAUgBaAGIAYgBeAFoATwA9ACyAHcAKQ/aD5wPXA8sDwgO9A8EDxQPNA9qD4YPtY/ggBcANgBSAGIAagBSAEEAJI/3D8IPnA9kD0wPGA74DvgO+A78DxwPTA9mD6iP4wAiAGwApADoARgBWAF4AZgBmAGYAXgBQ=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.260) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"gBHADKAHMANI/iD6wPbA88DxwPDA8MDxwPNA9mD4IPvQ/ZgAsAJgBOAE4ARgBPACCAGY/hD8YPlA90D1wPLA8UDwQPHA8cDzQPXA9yD7uP4wAiAFwAhAC0AOgBCAEoAUgBWAFYAVgBOAEMANwAngBYgBUP2g+UD2wPPA8UDxwPFA8kD0QPZg+OD6cP2Y/3gBkAIwA1AD0AK4ASgAeP5w/KD64PhA90D1wPRA9ED0QPXA9mD4oPqw/agAcAOgBkAJwAvADoAQgBKAE4ATgBOAEsAPwAxACSAF2ACw/CD5wPXA88DxwPFA8kDzQPXA9yD6kPzo/mgBUAPgBGAFoAVgBaAEEAMIAej+sPxg+mD4QPbA9MDzQPPA88D0wPXA9yD6sP3YAPADoAdAC8ANgBCAEoAUgBSAFIAUgBJAD8ALoAc=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.280) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"4AbADoAQgBOAGIAbgBmAGYAYgBWAEoAQwAogBsAIsAJA8sD28PyA5QDfwPOA7wDfgO3IAED3YPlADoARwA6AFoAagBSAE8AOoAawA3D9wPXA8sDxgOyA5oDogOqA54DmgO5A8kDxIPhoALgBIARAC8AOgBCAE4AWgBWAFYAVgBKAEYAQQAngBEALUAOA78D2mP+A5QDdwPTA8wDfgO2gBlgAcPyAEIAZgBOAFoAagBKAEEAOsALg+3D8wPWA7IDtQPCA6YDkgOqA7IDpgO3A80D1oPlYABACoAVADEAOwA6AE4AWgBSAFIAVgBLAD8APQAmgBsAKeP+A7yD4oPmA4wDfwPFA8oDmQPIwAxACoASAEIATgBKAFoAUwAvAC8AKOP5A9mD5QPaA7IDsQPGA7IDogO5A8IDvwPJA9qD4GP4=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.401) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"MANACEAMwA1ADoAQgBKAEYAQQA/ADsANQAxAC0AIIAbgBmAHQAloASD4IPrg+EDxgOyA6oDsgOzA8MD0wPYo/iAGwAlADIAQgBOAEoAQQA7ACBACOP7A98DwgOuA6YDngOWA5oDqgO7A9OD6GADgBcALwA6AEIASgBOAE4ARwA/ADkANQAxACaAGYATwAsgBaAFoAaD7IPig+cD2wPPA8sDzwPHA8cD0wPUg+fD8iADwAyAHwArADMAOQA7ADEALwAjQA/j/MPzA98DzQPCA7oDtgO2A70DywPWg+fD96AGgBUAIwApADEANwA1ADkANQA1ADEALwAmgB+AFsAPYAUgAGP/Q/KD64Png+MD3QPfA9kD2QPZA9yD4oPng+5D9+P/4AfADoAXgBqAHYAcgByAGIARQAogASP4w/CD64Pg=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.440) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"+AH4ABgAKP8o/nD9EP3w/BD9kP0I/pj+SP/4/8gAKAG4ARACcAJwAlACUALoAYgBKAEYASgBKAFIAagB6AHYAcgBaAG4ANj/6P4o/pD9sPxQ/HD80PxQ/Tj+KAB4AXACcAOwA5AD0AOwAzACeP/Q/ZD8IPvg+aD5IPrg+hD8UP2I/uj/KAFwAlAD4ATgBKAEUAJ4AJj/GP4w/dD8sP3Q/Xj++P/YAMgBsAJwA7ADoAdACfADeADY/8j+4Pvg+qD7wPbA9RD8IARgB8AIQAtAC0AJQAvAC6AH6P4g+UD2QPJA8EDwgO2A7cDxQPag+Tj+IATgB8AIQArADMAMQAggBCgAkP3Q/OD7IPug+hD8UP3IAWAFIAagBqAHoAegB0AMQAlQ/MD3wPXA90D34Pig+EDzwPUoAcAOgBHAD4AQwA0=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.498) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"gOwQ/YAWACOAFeAHgBGAGYAQSAFA8IDggOFA8MD2QPJA9FgBQArADEAKKABA9sD3eP4Y/6D72P9gB8ANgBBADGAFMAPgBJAD6P8w/DD8CADgBeAHEANo/3j+4AWAGoAbQPYA1YDiYPqg+4DrgOWA7Ej/gBoAJYAUoAaAE4AegBPo/4DsgOCA5EDyQPSA7IDsYPkgB0AMwAi4/qD48PygBGAEkPzg+tACwAvADUAJCAFI/iAEQAgwAmD6IPvQAsAJwAygB2j/UP2YACAHgBKAEMD0AN+A6jD9IPqA64DmgO94AIAUgBtADqAGgBSAHYASSABA8YDogOvA80D0gO/A8ND9QAlACyAHaAAw/bj/CABg+8D2IPoQA2AHYAbwA7ACYATACEAJ6AEw/NgAQAhACWAFqAAY//gAEAIgBcAMwAs=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.559) +{"sampleRate":16000,"streamId":"0.1.0.18.0","chunk":1,"chunk_durn_ms":20,"channelId":"","event":"media","payload":"wAjQA0gBOAAoAKAFQApgBuD6wPVg+WD7oPkg+MD3oPoQ/bADgBKAGUAIgOYA34DvYPjA9sDyQPQwA4AdACWAFqAE8ANACOAEoPnA8UD0WABADcALWP5g+SAEQAxQAoDtgOfA8CD64Pog+ED34PrgBkANoAf4AaAFwAvACRACeAFgBGAFIAagB6AFCP+g+zD8IPrA96D4oPrg++D6kPxgBYASgBlIAADbAN3A88D3wPHA8uD7gBEAJwAnwA84/yAFwAo4/0DywPJg+iAFQA7ACaD4QPfgB0AJwPOA4oDpwPcY/hj/kPzg+egAQA7ADLgAqP/ACMAL4AawA6AEoAfADYAQQAloAHD9CP6g+sDzQPHA86D44Ptg+mD48P3ADYARwPMA2YDmYPvg+UD04PmgB4AaACeAHaAHeP9AC0AM4Pk=","callId":"0.1.0.18.0","extraParams":""} (15:48:12.700) diff --git a/deepgram/__init__.py b/deepgram/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..95f056d3088b85b190cbc942275d0b7b6ab828f7 --- /dev/null +++ b/deepgram/__init__.py @@ -0,0 +1,383 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +# version +__version__ = "0.0.0" + +# entry point for the deepgram python sdk +import logging +from .utils import VerboseLogger +from .utils import ( + NOTICE, + SPAM, + SUCCESS, + VERBOSE, + WARNING, + ERROR, + FATAL, + CRITICAL, + INFO, + DEBUG, + NOTSET, +) + +from .client import Deepgram, DeepgramClient +from .client import DeepgramClientOptions, ClientOptionsFromEnv +from .client import ( + DeepgramError, + DeepgramTypeError, + DeepgramModuleError, + DeepgramApiError, + DeepgramUnknownApiError, +) +from .errors import DeepgramApiKeyError + +# listen/read client +from .client import ListenRouter, ReadRouter, SpeakRouter, AgentRouter + +# common +from .client import ( + TextSource, + BufferSource, + StreamSource, + FileSource, + UrlSource, +) +from .client import BaseResponse +from .client import ( + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, +) +from .client import ( + ModelInfo, + Hit, + Search, +) +from .client import ( + OpenResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) + +# speect-to-text WS +from .client import LiveClient, AsyncLiveClient # backward compat +from .client import ListenWebSocketClient, AsyncListenWebSocketClient +from .client import LiveTranscriptionEvents +from .client import LiveOptions, ListenWebSocketOptions +from .client import ( + #### top level + LiveResultResponse, + ListenWSMetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + #### common websocket response + # OpenResponse, + # CloseResponse, + # UnhandledResponse, + # ErrorResponse, + #### unique + ListenWSMetadata, + ListenWSAlternative, + ListenWSChannel, + ListenWSWord, +) + +# prerecorded +from .client import PreRecordedClient, AsyncPreRecordedClient # backward compat +from .client import ListenRESTClient, AsyncListenRESTClient +from .client import ( + # common + # UrlSource, + # BufferSource, + # StreamSource, + # TextSource, + # FileSource, + # unique + PreRecordedStreamSource, + PrerecordedSource, + ListenRestSource, + SpeakRESTSource, +) +from .client import ( + ListenRESTOptions, + PrerecordedOptions, +) +from .client import ( + #### top level + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, + #### shared + # Average, + # Alternative, + # Channel, + # Intent, + # Intents, + # IntentsInfo, + # Segment, + # SentimentInfo, + # Sentiment, + # Sentiments, + # SummaryInfo, + # Topic, + # Topics, + # TopicsInfo, + # Word, + #### unique + Entity, + Hit, + ListenRESTMetadata, + ModelInfo, + Paragraph, + Paragraphs, + ListenRESTResults, + Search, + Sentence, + Summaries, + SummaryV1, + SummaryV2, + Translation, + Utterance, + Warning, + ListenRESTAlternative, + ListenRESTChannel, + ListenRESTWord, +) + +# read +from .client import ReadClient, AsyncReadClient +from .client import AnalyzeClient, AsyncAnalyzeClient +from .client import ( + AnalyzeOptions, + AnalyzeStreamSource, + AnalyzeSource, +) +from .client import ( + #### top level + AsyncAnalyzeResponse, + SyncAnalyzeResponse, + AnalyzeResponse, + #### shared + # Average, + # Intent, + # Intents, + # IntentsInfo, + # Segment, + # SentimentInfo, + # Sentiment, + # Sentiments, + # SummaryInfo, + # Topic, + # Topics, + # TopicsInfo, + #### unique + AnalyzeMetadata, + AnalyzeResults, + AnalyzeSummary, +) + +# speak +## speak REST +from .client import ( + #### top level + SpeakRESTOptions, + SpeakOptions, # backward compat + #### common + # TextSource, + # BufferSource, + # StreamSource, + # FileSource, + #### unique + SpeakSource, + SpeakRestSource, +) + +from .client import ( + SpeakClient, # backward compat + SpeakRESTClient, + AsyncSpeakRESTClient, +) + +from .client import ( + SpeakResponse, # backward compat + SpeakRESTResponse, +) + +## speak WebSocket +from .client import SpeakWebSocketEvents, SpeakWebSocketMessage + +from .client import ( + SpeakWSOptions, +) + +from .client import ( + SpeakWebSocketClient, + AsyncSpeakWebSocketClient, + SpeakWSClient, + AsyncSpeakWSClient, +) + +from .client import ( + #### top level + SpeakWSMetadataResponse, + FlushedResponse, + ClearedResponse, + WarningResponse, + #### common websocket response + # OpenResponse, + # CloseResponse, + # UnhandledResponse, + # ErrorResponse, +) + +# manage +from .client import ManageClient, AsyncManageClient +from .client import ( + ProjectOptions, + KeyOptions, + ScopeOptions, + InviteOptions, + UsageRequestOptions, + UsageSummaryOptions, + UsageFieldsOptions, +) + +# manage client responses +from .client import ( + #### top level + Message, + ProjectsResponse, + ModelResponse, + ModelsResponse, + MembersResponse, + KeyResponse, + KeysResponse, + ScopesResponse, + InvitesResponse, + UsageRequest, + UsageResponse, + UsageRequestsResponse, + UsageSummaryResponse, + UsageFieldsResponse, + BalancesResponse, + #### shared + Project, + STTDetails, + TTSMetadata, + TTSDetails, + Member, + Key, + Invite, + Config, + STTUsageDetails, + Callback, + TokenDetail, + SpeechSegment, + TTSUsageDetails, + STTTokens, + TTSTokens, + UsageSummaryResults, + Resolution, + UsageModel, + Balance, +) + +# selfhosted +from .client import ( + OnPremClient, + AsyncOnPremClient, + SelfHostedClient, + AsyncSelfHostedClient, +) + + +# agent +from .client import AgentWebSocketEvents + +# websocket +from .client import ( + AgentWebSocketClient, + AsyncAgentWebSocketClient, +) + +from .client import ( + #### common websocket response + # OpenResponse, + # CloseResponse, + # ErrorResponse, + # UnhandledResponse, + #### unique + WelcomeResponse, + SettingsAppliedResponse, + ConversationTextResponse, + UserStartedSpeakingResponse, + AgentThinkingResponse, + FunctionCallRequest, + AgentStartedSpeakingResponse, + AgentAudioDoneResponse, + InjectionRefusedResponse, +) + +from .client import ( + # top level + SettingsOptions, + UpdatePromptOptions, + UpdateSpeakOptions, + InjectAgentMessageOptions, + FunctionCallResponse, + AgentKeepAlive, + # sub level + Listen, + ListenProvider, + Speak, + SpeakProvider, + Header, + Item, + Properties, + Parameters, + Function, + Think, + ThinkProvider, + Agent, + Input, + Output, + Audio, + Endpoint, +) + +# utilities +# pylint: disable=wrong-import-position +from .audio import Microphone, DeepgramMicrophoneError +from .audio import ( + INPUT_LOGGING, + INPUT_CHANNELS, + INPUT_RATE, + INPUT_CHUNK, +) + +LOGGING = INPUT_LOGGING +CHANNELS = INPUT_CHANNELS +RATE = INPUT_RATE +CHUNK = INPUT_CHUNK + +from .audio import Speaker +from .audio import ( + OUTPUT_LOGGING, + OUTPUT_CHANNELS, + OUTPUT_RATE, + OUTPUT_CHUNK, +) + +# pylint: enable=wrong-import-position diff --git a/deepgram/__pycache__/__init__.cpython-313.pyc b/deepgram/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31a6c700b770e3e184f5686d7f7bfc93a3f3e1b4 Binary files /dev/null and b/deepgram/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/__pycache__/client.cpython-313.pyc b/deepgram/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0d60ba6a44c109d47570cbc9c81ae36894968f04 Binary files /dev/null and b/deepgram/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/__pycache__/errors.cpython-313.pyc b/deepgram/__pycache__/errors.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f8b73459157a95be3d2371cd580457afd4fcefe Binary files /dev/null and b/deepgram/__pycache__/errors.cpython-313.pyc differ diff --git a/deepgram/__pycache__/options.cpython-313.pyc b/deepgram/__pycache__/options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e56ba5cff55834b0502bba6e13615f3871db4728 Binary files /dev/null and b/deepgram/__pycache__/options.cpython-313.pyc differ diff --git a/deepgram/audio/__init__.py b/deepgram/audio/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dcadaa4c0e818d3bedfe264e8b470b6f3d995e02 --- /dev/null +++ b/deepgram/audio/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .microphone import Microphone +from .microphone import DeepgramMicrophoneError +from .microphone import ( + LOGGING as INPUT_LOGGING, + CHANNELS as INPUT_CHANNELS, + RATE as INPUT_RATE, + CHUNK as INPUT_CHUNK, +) + +from .speaker import Speaker +from .speaker import DeepgramSpeakerError +from .speaker import ( + LOGGING as OUTPUT_LOGGING, + CHANNELS as OUTPUT_CHANNELS, + RATE as OUTPUT_RATE, + CHUNK as OUTPUT_CHUNK, + PLAYBACK_DELTA as OUTPUT_PLAYBACK_DELTA, +) diff --git a/deepgram/audio/__pycache__/__init__.cpython-313.pyc b/deepgram/audio/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2cac12171838bb701242d2f62beec54f0b86069 Binary files /dev/null and b/deepgram/audio/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/audio/microphone/__init__.py b/deepgram/audio/microphone/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d5ead2f1dacaf1e969e23938f0b8cf8b8d137049 --- /dev/null +++ b/deepgram/audio/microphone/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .microphone import Microphone +from .constants import LOGGING, CHANNELS, RATE, CHUNK +from .errors import DeepgramMicrophoneError diff --git a/deepgram/audio/microphone/__pycache__/__init__.cpython-313.pyc b/deepgram/audio/microphone/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..133465cb8ae43a430e6a4c380fb6bc421c9e7eb6 Binary files /dev/null and b/deepgram/audio/microphone/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/audio/microphone/__pycache__/constants.cpython-313.pyc b/deepgram/audio/microphone/__pycache__/constants.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd8f4af63c32411065909a1f8b5e7813329139f2 Binary files /dev/null and b/deepgram/audio/microphone/__pycache__/constants.cpython-313.pyc differ diff --git a/deepgram/audio/microphone/__pycache__/errors.cpython-313.pyc b/deepgram/audio/microphone/__pycache__/errors.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1f6df7606a254abcbcdebf1f5f4ce66861ea893 Binary files /dev/null and b/deepgram/audio/microphone/__pycache__/errors.cpython-313.pyc differ diff --git a/deepgram/audio/microphone/__pycache__/microphone.cpython-313.pyc b/deepgram/audio/microphone/__pycache__/microphone.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9df17cd31d14cc3730398ad45d4a99c1f96b166f Binary files /dev/null and b/deepgram/audio/microphone/__pycache__/microphone.cpython-313.pyc differ diff --git a/deepgram/audio/microphone/constants.py b/deepgram/audio/microphone/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..4aed8106a85943f9538256cf0a1967cec1413a6b --- /dev/null +++ b/deepgram/audio/microphone/constants.py @@ -0,0 +1,11 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from ...utils import verboselogs + +# Constants for microphone +LOGGING = verboselogs.WARNING +CHANNELS = 1 +RATE = 16000 +CHUNK = 8194 diff --git a/deepgram/audio/microphone/errors.py b/deepgram/audio/microphone/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..6e7fe5ef132755a5ca080cd82651827d5cb05b08 --- /dev/null +++ b/deepgram/audio/microphone/errors.py @@ -0,0 +1,21 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + + +# exceptions for microphone +class DeepgramMicrophoneError(Exception): + """ + Exception raised for known errors related to Microphone library. + + Attributes: + message (str): The error message describing the exception. + """ + + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramMicrophoneError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" diff --git a/deepgram/audio/microphone/microphone.py b/deepgram/audio/microphone/microphone.py new file mode 100644 index 0000000000000000000000000000000000000000..3a3ad341ce952a6b3c338579f3f43b735ec057cd --- /dev/null +++ b/deepgram/audio/microphone/microphone.py @@ -0,0 +1,302 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import inspect +import asyncio +import threading +from typing import Optional, Callable, Union, TYPE_CHECKING +import logging + +from ...utils import verboselogs + +from .constants import LOGGING, CHANNELS, RATE, CHUNK + +if TYPE_CHECKING: + import pyaudio + + +class Microphone: # pylint: disable=too-many-instance-attributes + """ + This implements a microphone for local audio input. This uses PyAudio under the hood. + """ + + _logger: verboselogs.VerboseLogger + + _audio: Optional["pyaudio.PyAudio"] = None + _stream: Optional["pyaudio.Stream"] = None + + _chunk: int + _rate: int + _format: int + _channels: int + _input_device_index: Optional[int] + _is_muted: bool + + _asyncio_loop: asyncio.AbstractEventLoop + _asyncio_thread: Optional[threading.Thread] = None + _exit: threading.Event + + _push_callback_org: Optional[Callable] = None + _push_callback: Optional[Callable] = None + + def __init__( + self, + push_callback: Optional[Callable] = None, + verbose: int = LOGGING, + rate: int = RATE, + chunk: int = CHUNK, + channels: int = CHANNELS, + input_device_index: Optional[int] = None, + ): # pylint: disable=too-many-positional-arguments + # dynamic import of pyaudio as not to force the requirements on the SDK (and users) + import pyaudio # pylint: disable=import-outside-toplevel + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(verbose) + + self._exit = threading.Event() + + self._audio = pyaudio.PyAudio() + self._chunk = chunk + self._rate = rate + self._format = pyaudio.paInt16 + self._channels = channels + self._is_muted = False + + self._input_device_index = input_device_index + self._push_callback_org = push_callback + + def _start_asyncio_loop(self) -> None: + self._asyncio_loop = asyncio.new_event_loop() + self._asyncio_loop.run_forever() + + def is_active(self) -> bool: + """ + is_active - returns the state of the stream + + Args: + None + + Returns: + True if the stream is active, False otherwise + """ + self._logger.debug("Microphone.is_active ENTER") + + if self._stream is None: + self._logger.error("stream is None") + self._logger.debug("Microphone.is_active LEAVE") + return False + + val = self._stream.is_active() + self._logger.info("is_active: %s", val) + self._logger.info("is_exiting: %s", self._exit.is_set()) + self._logger.debug("Microphone.is_active LEAVE") + return val + + def set_callback(self, push_callback: Callable) -> None: + """ + set_callback - sets the callback function to be called when data is received. + + Args: + push_callback (Callable): The callback function to be called when data is received. + This should be the websocket send function. + + Returns: + None + """ + self._push_callback_org = push_callback + + def start(self) -> bool: + """ + starts - starts the microphone stream + + Returns: + bool: True if the stream was started, False otherwise + """ + self._logger.debug("Microphone.start ENTER") + + self._logger.info("format: %s", self._format) + self._logger.info("channels: %d", self._channels) + self._logger.info("rate: %d", self._rate) + self._logger.info("chunk: %d", self._chunk) + # self._logger.info("input_device_id: %d", self._input_device_index) + + if self._push_callback_org is None: + self._logger.error("start failed. No callback set.") + self._logger.debug("Microphone.start LEAVE") + return False + + if inspect.iscoroutinefunction(self._push_callback_org): + self._logger.verbose("async/await callback - wrapping") + # Run our own asyncio loop. + self._asyncio_thread = threading.Thread(target=self._start_asyncio_loop) + self._asyncio_thread.start() + + self._push_callback = lambda data: ( + asyncio.run_coroutine_threadsafe( + self._push_callback_org(data), self._asyncio_loop + ).result() + if self._push_callback_org + else None + ) + else: + self._logger.verbose("regular threaded callback") + self._asyncio_thread = None + self._push_callback = self._push_callback_org + + if self._audio is not None: + self._stream = self._audio.open( + format=self._format, + channels=self._channels, + rate=self._rate, + input=True, + output=False, + frames_per_buffer=self._chunk, + input_device_index=self._input_device_index, + stream_callback=self._callback, + ) + + if self._stream is None: + self._logger.error("start failed. No stream created.") + self._logger.debug("Microphone.start LEAVE") + return False + + self._exit.clear() + if self._stream is not None: + self._stream.start_stream() + + self._logger.notice("start succeeded") + self._logger.debug("Microphone.start LEAVE") + return True + + def mute(self) -> bool: + """ + mute - mutes the microphone stream + + Returns: + bool: True if the stream was muted, False otherwise + """ + self._logger.verbose("Microphone.mute ENTER") + + if self._stream is None: + self._logger.error("mute failed. Library not initialized.") + self._logger.verbose("Microphone.mute LEAVE") + return False + + self._is_muted = True + + self._logger.notice("mute succeeded") + self._logger.verbose("Microphone.mute LEAVE") + return True + + def unmute(self) -> bool: + """ + unmute - unmutes the microphone stream + + Returns: + bool: True if the stream was unmuted, False otherwise + """ + self._logger.verbose("Microphone.unmute ENTER") + + if self._stream is None: + self._logger.error("unmute failed. Library not initialized.") + self._logger.verbose("Microphone.unmute LEAVE") + return False + + self._is_muted = False + + self._logger.notice("unmute succeeded") + self._logger.verbose("Microphone.unmute LEAVE") + return True + + def is_muted(self) -> bool: + """ + is_muted - returns the state of the stream + + Args: + None + + Returns: + True if the stream is muted, False otherwise + """ + self._logger.spam("Microphone.is_muted ENTER") + + if self._stream is None: + self._logger.spam("is_muted: stream is None") + self._logger.spam("Microphone.is_muted LEAVE") + return False + + val = self._is_muted + + self._logger.spam("is_muted: %s", val) + self._logger.spam("Microphone.is_muted LEAVE") + return val + + def finish(self) -> bool: + """ + finish - stops the microphone stream + + Returns: + bool: True if the stream was stopped, False otherwise + """ + self._logger.debug("Microphone.finish ENTER") + + self._logger.notice("signal exit") + self._exit.set() + + # Stop the stream. + if self._stream is not None: + self._logger.notice("stopping stream...") + self._stream.stop_stream() + self._stream.close() + self._logger.notice("stream stopped") + + # clean up the thread + if ( + # inspect.iscoroutinefunction(self._push_callback_org) + # and + self._asyncio_thread + is not None + ): + self._logger.notice("stopping _asyncio_loop...") + self._asyncio_loop.call_soon_threadsafe(self._asyncio_loop.stop) + self._asyncio_thread.join() + self._logger.notice("_asyncio_thread joined") + self._stream = None + self._asyncio_thread = None + + self._logger.notice("finish succeeded") + self._logger.debug("Microphone.finish LEAVE") + + return True + + def _callback( + self, input_data, frame_count, time_info, status_flags + ): # pylint: disable=unused-argument + """ + The callback used to process data in callback mode. + """ + # dynamic import of pyaudio as not to force the requirements on the SDK (and users) + import pyaudio # pylint: disable=import-outside-toplevel + + if self._exit.is_set(): + self._logger.notice("_callback exit is Set. stopping...") + return None, pyaudio.paAbort + + if input_data is None: + self._logger.warning("input_data is None") + return None, pyaudio.paContinue + + try: + if self._is_muted: + size = len(input_data) + input_data = b"\x00" * size + + self._push_callback(input_data) + except Exception as e: + self._logger.error("Error while sending: %s", str(e)) + raise + + return input_data, pyaudio.paContinue diff --git a/deepgram/audio/speaker/__init__.py b/deepgram/audio/speaker/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f9adc8e539f49484a503dd807780dfb683c48ffc --- /dev/null +++ b/deepgram/audio/speaker/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .speaker import Speaker +from .errors import DeepgramSpeakerError +from .constants import LOGGING, CHANNELS, RATE, CHUNK, PLAYBACK_DELTA diff --git a/deepgram/audio/speaker/__pycache__/__init__.cpython-313.pyc b/deepgram/audio/speaker/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62985939f36f1edc1bea865c5e7cf820b39a65a9 Binary files /dev/null and b/deepgram/audio/speaker/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/audio/speaker/__pycache__/constants.cpython-313.pyc b/deepgram/audio/speaker/__pycache__/constants.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5190ce9fbb0d34b73a7fb967672b0c7ce9bdba9a Binary files /dev/null and b/deepgram/audio/speaker/__pycache__/constants.cpython-313.pyc differ diff --git a/deepgram/audio/speaker/__pycache__/errors.cpython-313.pyc b/deepgram/audio/speaker/__pycache__/errors.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88ee6a2d316c49c542ecb872264497fb659d09b1 Binary files /dev/null and b/deepgram/audio/speaker/__pycache__/errors.cpython-313.pyc differ diff --git a/deepgram/audio/speaker/__pycache__/speaker.cpython-313.pyc b/deepgram/audio/speaker/__pycache__/speaker.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5dc454d51c2309ba9ec1e37e108243c5464baa9c Binary files /dev/null and b/deepgram/audio/speaker/__pycache__/speaker.cpython-313.pyc differ diff --git a/deepgram/audio/speaker/constants.py b/deepgram/audio/speaker/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..a033c721f5c0520d56d9e0db7af6e35b5484f87b --- /dev/null +++ b/deepgram/audio/speaker/constants.py @@ -0,0 +1,15 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from ...utils import verboselogs + +# Constants for speaker +LOGGING = verboselogs.WARNING +TIMEOUT = 0.050 +CHANNELS = 1 +RATE = 16000 +CHUNK = 8194 + +# Constants for speaker +PLAYBACK_DELTA = 2000 diff --git a/deepgram/audio/speaker/errors.py b/deepgram/audio/speaker/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..4d9b95efc0994625abcddc61a8c746dbb2310972 --- /dev/null +++ b/deepgram/audio/speaker/errors.py @@ -0,0 +1,21 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + + +# exceptions for speaker +class DeepgramSpeakerError(Exception): + """ + Exception raised for known errors related to Speaker library. + + Attributes: + message (str): The error message describing the exception. + """ + + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramSpeakerError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" diff --git a/deepgram/audio/speaker/speaker.py b/deepgram/audio/speaker/speaker.py new file mode 100644 index 0000000000000000000000000000000000000000..e16d589883e619f20599e2823511fc286a2d6642 --- /dev/null +++ b/deepgram/audio/speaker/speaker.py @@ -0,0 +1,380 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import asyncio +import inspect +import queue +import threading +from typing import Optional, Callable, Union, TYPE_CHECKING +import logging +from datetime import datetime + +import websockets + +from ...utils import verboselogs +from .constants import LOGGING, CHANNELS, RATE, CHUNK, TIMEOUT, PLAYBACK_DELTA + +from ..microphone import Microphone + +if TYPE_CHECKING: + import pyaudio + +HALF_SECOND = 0.5 + + +class Speaker: # pylint: disable=too-many-instance-attributes + """ + This implements a speaker for local audio output. This uses PyAudio under the hood. + """ + + _logger: verboselogs.VerboseLogger + + _audio: Optional["pyaudio.PyAudio"] = None + _stream: Optional["pyaudio.Stream"] = None + + _chunk: int + _rate: int + _channels: int + _output_device_index: Optional[int] = None + + # last time we received audio + _last_datagram: datetime = datetime.now() + _last_play_delta_in_ms: int + _lock_wait: threading.Lock + + _queue: queue.Queue + _exit: threading.Event + + _thread: Optional[threading.Thread] = None + # _asyncio_loop: asyncio.AbstractEventLoop + # _asyncio_thread: threading.Thread + _receiver_thread: Optional[threading.Thread] = None + _loop: Optional[asyncio.AbstractEventLoop] = None + + _push_callback_org: Optional[Callable] = None + _push_callback: Optional[Callable] = None + _pull_callback_org: Optional[Callable] = None + _pull_callback: Optional[Callable] = None + + _microphone: Optional[Microphone] = None + + def __init__( + self, + pull_callback: Optional[Callable] = None, + push_callback: Optional[Callable] = None, + verbose: int = LOGGING, + rate: int = RATE, + chunk: int = CHUNK, + channels: int = CHANNELS, + last_play_delta_in_ms: int = PLAYBACK_DELTA, + output_device_index: Optional[int] = None, + microphone: Optional[Microphone] = None, + ): # pylint: disable=too-many-positional-arguments + # dynamic import of pyaudio as not to force the requirements on the SDK (and users) + import pyaudio # pylint: disable=import-outside-toplevel + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(verbose) + + self._exit = threading.Event() + self._queue = queue.Queue() + + self._last_datagram = datetime.now() + self._lock_wait = threading.Lock() + + self._microphone = microphone + + self._audio = pyaudio.PyAudio() + self._chunk = chunk + self._rate = rate + self._format = pyaudio.paInt16 + self._channels = channels + self._last_play_delta_in_ms = last_play_delta_in_ms + self._output_device_index = output_device_index + + self._push_callback_org = push_callback + self._pull_callback_org = pull_callback + + def set_push_callback(self, push_callback: Callable) -> None: + """ + set_push_callback - sets the callback function to be called when data is sent. + + Args: + push_callback (Callable): The callback function to be called when data is send. + This should be the websocket handle message function. + + Returns: + None + """ + self._push_callback_org = push_callback + + def set_pull_callback(self, pull_callback: Callable) -> None: + """ + set_pull_callback - sets the callback function to be called when data is received. + + Args: + pull_callback (Callable): The callback function to be called when data is received. + This should be the websocket recv function. + + Returns: + None + """ + self._pull_callback_org = pull_callback + + def start(self, active_loop: Optional[asyncio.AbstractEventLoop] = None) -> bool: + """ + starts - starts the Speaker stream + + Args: + socket (Union[SyncClientConnection, AsyncClientConnection]): The socket to receive audio data from. + + Returns: + bool: True if the stream was started, False otherwise + """ + self._logger.debug("Speaker.start ENTER") + + self._logger.info("format: %s", self._format) + self._logger.info("channels: %d", self._channels) + self._logger.info("rate: %d", self._rate) + self._logger.info("chunk: %d", self._chunk) + # self._logger.info("output_device_id: %d", self._output_device_index) + + # Automatically get the current running event loop + if inspect.iscoroutinefunction(self._push_callback_org) and active_loop is None: + self._logger.verbose("get default running asyncio loop") + self._loop = asyncio.get_running_loop() + + self._exit.clear() + self._queue = queue.Queue() + + if self._audio is not None: + self._stream = self._audio.open( + format=self._format, + channels=self._channels, + rate=self._rate, + input=False, + output=True, + frames_per_buffer=self._chunk, + output_device_index=self._output_device_index, + ) + + if self._stream is None: + self._logger.error("start failed. No stream created.") + self._logger.debug("Speaker.start LEAVE") + return False + + self._push_callback = self._push_callback_org + self._pull_callback = self._pull_callback_org + + # start the play thread + self._thread = threading.Thread( + target=self._play, args=(self._queue, self._stream, self._exit), daemon=True + ) + self._thread.start() + + # Start the stream + if self._stream is not None: + self._stream.start_stream() + + # Start the receiver thread within the start function + self._logger.verbose("Starting receiver thread...") + self._receiver_thread = threading.Thread(target=self._start_receiver) + self._receiver_thread.start() + + self._logger.notice("start succeeded") + self._logger.debug("Speaker.start LEAVE") + + return True + + def wait_for_complete_with_mute(self, mic: Microphone): + """ + This method will mute/unmute a Microphone and block until the speak is done playing sound. + """ + self._logger.debug("Speaker.wait_for_complete ENTER") + + if self._microphone is not None: + mic.mute() + self.wait_for_complete() + if self._microphone is not None: + mic.unmute() + + self._logger.debug("Speaker.wait_for_complete LEAVE") + + def wait_for_complete(self): + """ + This method will block until the speak is done playing sound. + """ + self._logger.debug("Speaker.wait_for_complete ENTER") + + delta_in_ms = float(self._last_play_delta_in_ms) + self._logger.debug("Last Play delta: %f", delta_in_ms) + + # set to now + with self._lock_wait: + self._last_datagram = datetime.now() + + while True: + # sleep for a bit + self._exit.wait(HALF_SECOND) + + # check if we should exit + if self._exit.is_set(): + self._logger.debug("Exiting wait_for_complete _exit is set") + break + + # check the time + with self._lock_wait: + delta = datetime.now() - self._last_datagram + diff_in_ms = delta.total_seconds() * 1000 + if diff_in_ms < delta_in_ms: + self._logger.debug("LastPlay delta is less than threshold") + continue + + # if we get here, we are done playing audio + self._logger.debug("LastPlay delta is greater than threshold. Exit wait!") + break + + self._logger.debug("Speaker.wait_for_complete LEAVE") + + def _start_receiver(self): + # Check if the socket is an asyncio WebSocket + if inspect.iscoroutinefunction(self._pull_callback_org): + self._logger.verbose("Starting asyncio receiver...") + asyncio.run_coroutine_threadsafe(self._start_asyncio_receiver(), self._loop) + else: + self._logger.verbose("Starting threaded receiver...") + self._start_threaded_receiver() + + async def _start_asyncio_receiver(self): + try: + while True: + if self._exit.is_set(): + self._logger.verbose("Exiting receiver thread...") + break + + message = await self._pull_callback() + if message is None: + self._logger.verbose("No message received...") + continue + + if isinstance(message, str): + self._logger.verbose("Received control message...") + await self._push_callback(message) + elif isinstance(message, bytes): + self._logger.verbose("Received audio data...") + await self._push_callback(message) + self.add_audio_to_queue(message) + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.debug("send() exiting gracefully: %d", e.code) + except websockets.exceptions.ConnectionClosed as e: + if e.code in [1000, 1001]: + self._logger.debug("send() exiting gracefully: %d", e.code) + return + self._logger.error("_start_asyncio_receiver - ConnectionClosed: %s", str(e)) + except websockets.exceptions.WebSocketException as e: + self._logger.error( + "_start_asyncio_receiver- WebSocketException: %s", str(e) + ) + except Exception as e: # pylint: disable=broad-except + self._logger.error("_start_asyncio_receiver exception: %s", str(e)) + + def _start_threaded_receiver(self): + try: + while True: + if self._exit.is_set(): + self._logger.verbose("Exiting receiver thread...") + break + + message = self._pull_callback() + if message is None: + self._logger.verbose("No message received...") + continue + + if isinstance(message, str): + self._logger.verbose("Received control message...") + self._push_callback(message) + elif isinstance(message, bytes): + self._logger.verbose("Received audio data...") + self._push_callback(message) + self.add_audio_to_queue(message) + except Exception as e: # pylint: disable=broad-except + self._logger.notice("_start_threaded_receiver exception: %s", str(e)) + + def add_audio_to_queue(self, data: bytes) -> None: + """ + add_audio_to_queue - adds audio data to the Speaker queue + + Args: + data (bytes): The audio data to add to the queue + """ + self._queue.put(data) + + def finish(self) -> bool: + """ + finish - stops the Speaker stream + + Returns: + bool: True if the stream was stopped, False otherwise + """ + self._logger.debug("Speaker.finish ENTER") + + self._logger.notice("signal exit") + self._exit.set() + + if self._stream is not None: + self._logger.notice("stopping stream...") + self._stream.stop_stream() + self._stream.close() + self._logger.notice("stream stopped") + + if self._thread is not None: + self._logger.notice("joining _thread...") + self._thread.join() + self._logger.notice("thread stopped") + + if self._receiver_thread is not None: + self._logger.notice("stopping _receiver_thread...") + self._receiver_thread.join() + self._logger.notice("_receiver_thread joined") + + with self._queue.mutex: + self._queue.queue.clear() + + self._stream = None + self._thread = None + self._receiver_thread = None + + self._logger.notice("finish succeeded") + self._logger.debug("Speaker.finish LEAVE") + + return True + + def _play(self, audio_out, stream, stop): + """ + _play - plays audio data from the Speaker queue callback for portaudio + """ + while not stop.is_set(): + try: + if self._microphone is not None and self._microphone.is_muted(): + with self._lock_wait: + delta = datetime.now() - self._last_datagram + diff_in_ms = delta.total_seconds() * 1000 + if diff_in_ms > float(self._last_play_delta_in_ms): + self._logger.debug( + "LastPlay delta is greater than threshold. Unmute!" + ) + self._microphone.unmute() + + data = audio_out.get(True, TIMEOUT) + with self._lock_wait: + self._last_datagram = datetime.now() + if self._microphone is not None and not self._microphone.is_muted(): + self._logger.debug("New speaker sound detected. Mute!") + self._microphone.mute() + stream.write(data) + except queue.Empty: + pass + except Exception as e: # pylint: disable=broad-except + self._logger.error("_play exception: %s", str(e)) diff --git a/deepgram/client.py b/deepgram/client.py new file mode 100644 index 0000000000000000000000000000000000000000..c9d20fd11ffc69dcbe8ef53b1b0fbe1b87c00bd0 --- /dev/null +++ b/deepgram/client.py @@ -0,0 +1,669 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import Optional +from importlib import import_module +import os +import logging +import deprecation # type: ignore + +from . import __version__ +from .utils import verboselogs + +# common +# pylint: disable=unused-import +from .clients import ( + TextSource, + BufferSource, + StreamSource, + FileSource, + UrlSource, +) +from .clients import BaseResponse +from .clients import ( + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, +) +from .clients import ( + ModelInfo, + Hit, + Search, +) +from .clients import ( + OpenResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) +from .clients import ( + DeepgramError, + DeepgramTypeError, + DeepgramModuleError, + DeepgramApiError, + DeepgramUnknownApiError, +) + +# listen client +from .clients import ListenRouter, ReadRouter, SpeakRouter, AgentRouter + +# speech-to-text +from .clients import LiveClient, AsyncLiveClient # backward compat +from .clients import ( + ListenWebSocketClient, + AsyncListenWebSocketClient, +) +from .clients import ( + ListenWebSocketOptions, + LiveOptions, + LiveTranscriptionEvents, +) + +# live client responses +from .clients import ( + #### top level + LiveResultResponse, + ListenWSMetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + #### common websocket response + # OpenResponse, + # CloseResponse, + # ErrorResponse, + # UnhandledResponse, + #### unique + ListenWSMetadata, + ListenWSAlternative, + ListenWSChannel, + ListenWSWord, +) + +# prerecorded +from .clients import ( + # common + # UrlSource, + # BufferSource, + # StreamSource, + # TextSource, + # FileSource, + # unique + PreRecordedStreamSource, + PrerecordedSource, + ListenRestSource, +) + +from .clients import ( + PreRecordedClient, + AsyncPreRecordedClient, +) # backward compat +from .clients import ( + ListenRESTClient, + AsyncListenRESTClient, +) +from .clients import ( + ListenRESTOptions, + PrerecordedOptions, +) + +# rest client responses +from .clients import ( + #### top level + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, + #### shared + # Average, + # Intent, + # Intents, + # IntentsInfo, + # Segment, + # SentimentInfo, + # Sentiment, + # Sentiments, + # SummaryInfo, + # Topic, + # Topics, + # TopicsInfo, + #### between rest and websocket + # ModelInfo, + # Alternative, + # Hit, + # Search, + # Channel, + # Word, + # unique + Entity, + ListenRESTMetadata, + Paragraph, + Paragraphs, + ListenRESTResults, + Sentence, + Summaries, + SummaryV1, + SummaryV2, + Translation, + Utterance, + Warning, + ListenRESTAlternative, + ListenRESTChannel, + ListenRESTWord, +) + +# read +from .clients import ReadClient, AsyncReadClient +from .clients import AnalyzeClient, AsyncAnalyzeClient +from .clients import ( + AnalyzeOptions, + AnalyzeStreamSource, + AnalyzeSource, +) + +# read client responses +from .clients import ( + #### top level + AsyncAnalyzeResponse, + SyncAnalyzeResponse, + AnalyzeResponse, + #### shared + # Average, + # Intent, + # Intents, + # IntentsInfo, + # Segment, + # SentimentInfo, + # Sentiment, + # Sentiments, + # SummaryInfo, + # Topic, + # Topics, + # TopicsInfo, + #### unique + AnalyzeMetadata, + AnalyzeResults, + AnalyzeSummary, +) + +# speak +## speak REST +from .clients import ( + #### top level + SpeakRESTOptions, + SpeakOptions, # backward compat + #### common + # TextSource, + # BufferSource, + # StreamSource, + # FileSource, + #### unique + SpeakSource, + SpeakRestSource, + SpeakRESTSource, +) + +from .clients import ( + SpeakClient, # backward compat + SpeakRESTClient, + AsyncSpeakRESTClient, +) + +from .clients import ( + SpeakResponse, # backward compat + SpeakRESTResponse, +) + +## speak WebSocket +from .clients import SpeakWebSocketEvents, SpeakWebSocketMessage + +from .clients import ( + SpeakWSOptions, +) + +from .clients import ( + SpeakWebSocketClient, + AsyncSpeakWebSocketClient, + SpeakWSClient, + AsyncSpeakWSClient, +) + +from .clients import ( + #### top level + SpeakWSMetadataResponse, + FlushedResponse, + ClearedResponse, + WarningResponse, + #### common websocket response + # OpenResponse, + # CloseResponse, + # UnhandledResponse, + # ErrorResponse, +) + +# auth client classes +from .clients import AuthRESTClient, AsyncAuthRESTClient + +# auth client responses +from .clients import ( + GrantTokenResponse, +) + +# manage client classes/input +from .clients import ManageClient, AsyncManageClient +from .clients import ( + ProjectOptions, + KeyOptions, + ScopeOptions, + InviteOptions, + UsageRequestOptions, + UsageSummaryOptions, + UsageFieldsOptions, +) + +# manage client responses +from .clients import ( + #### top level + Message, + ProjectsResponse, + ModelResponse, + ModelsResponse, + MembersResponse, + KeyResponse, + KeysResponse, + ScopesResponse, + InvitesResponse, + UsageRequest, + UsageResponse, + UsageRequestsResponse, + UsageSummaryResponse, + UsageFieldsResponse, + BalancesResponse, + #### shared + Project, + STTDetails, + TTSMetadata, + TTSDetails, + Member, + Key, + Invite, + Config, + STTUsageDetails, + Callback, + TokenDetail, + SpeechSegment, + TTSUsageDetails, + STTTokens, + TTSTokens, + UsageSummaryResults, + Resolution, + UsageModel, + Balance, +) + +# on-prem +from .clients import ( + OnPremClient, + AsyncOnPremClient, + SelfHostedClient, + AsyncSelfHostedClient, +) + + +# agent +from .clients import AgentWebSocketEvents + +# websocket +from .clients import ( + AgentWebSocketClient, + AsyncAgentWebSocketClient, +) + +from .clients import ( + #### common websocket response + # OpenResponse, + # CloseResponse, + # ErrorResponse, + # UnhandledResponse, + #### unique + WelcomeResponse, + SettingsAppliedResponse, + ConversationTextResponse, + UserStartedSpeakingResponse, + AgentThinkingResponse, + FunctionCallRequest, + AgentStartedSpeakingResponse, + AgentAudioDoneResponse, + InjectionRefusedResponse, +) + +from .clients import ( + # top level + SettingsOptions, + UpdatePromptOptions, + UpdateSpeakOptions, + InjectAgentMessageOptions, + FunctionCallResponse, + AgentKeepAlive, + # sub level + Listen, + ListenProvider, + Speak, + SpeakProvider, + Header, + Item, + Properties, + Parameters, + Function, + Think, + ThinkProvider, + Agent, + Input, + Output, + Audio, + Endpoint, +) + + +# client errors and options +from .options import DeepgramClientOptions, ClientOptionsFromEnv +from .errors import DeepgramApiKeyError + +# pylint: enable=unused-import + + +class Deepgram: # pylint: disable=broad-exception-raised + """ + The Deepgram class is no longer a class in version 3 of this SDK. + """ + + def __init__(self, *anything): + raise Exception( + """ + FATAL ERROR: + You are attempting to instantiate a Deepgram object, which is no longer a class in version 3 of this SDK. + + To fix this issue: + 1. You need to revert to the previous version 2 of the SDK: pip install deepgram-sdk==2.12.0 + 2. or, update your application's code to use version 3 of this SDK. See the README for more information. + + Things to consider: + + - This Version 3 of the SDK requires Python 3.10 or higher. + Older versions (3.9 and lower) of Python are nearing end-of-life: https://devguide.python.org/versions/ + Understand the risks of using a version of Python nearing EOL. + + - Version 2 of the SDK will receive maintenance updates in the form of security fixes only. + No new features will be added to version 2 of the SDK. + """ + ) + + +class DeepgramClient: + """ + Represents a client for interacting with the Deepgram API. + + This class provides a client for making requests to the Deepgram API with various configuration options. + + Attributes: + api_key (str): The Deepgram API key used for authentication. + config_options (DeepgramClientOptions): An optional configuration object specifying client options. + + Raises: + DeepgramApiKeyError: If the API key is missing or invalid. + + Methods: + listen: Returns a ListenClient instance for interacting with Deepgram's transcription services. + + manage: (Preferred) Returns a Threaded ManageClient instance for managing Deepgram resources. + selfhosted: (Preferred) Returns an Threaded SelfHostedClient instance for interacting with Deepgram's on-premises API. + + asyncmanage: Returns an (Async) ManageClient instance for managing Deepgram resources. + asyncselfhosted: Returns an (Async) SelfHostedClient instance for interacting with Deepgram's on-premises API. + """ + + _config: DeepgramClientOptions + _logger: verboselogs.VerboseLogger + + def __init__( + self, + api_key: str = "", + config: Optional[DeepgramClientOptions] = None, + ): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + + if api_key == "" and config is not None: + self._logger.info("Attempting to set API key from config object") + api_key = config.api_key + if api_key == "": + self._logger.info("Attempting to set API key from environment variable") + api_key = os.getenv("DEEPGRAM_API_KEY", "") + if api_key == "": + self._logger.warning("WARNING: API key is missing") + + self.api_key = api_key + if config is None: # Use default configuration + self._config = DeepgramClientOptions(self.api_key) + else: + config.set_apikey(self.api_key) + self._config = config + + @property + def listen(self): + """ + Returns a Listen dot-notation router for interacting with Deepgram's transcription services. + """ + return ListenRouter(self._config) + + @property + def read(self): + """ + Returns a Read dot-notation router for interacting with Deepgram's read services. + """ + return ReadRouter(self._config) + + @property + def speak(self): + """ + Returns a Speak dot-notation router for interacting with Deepgram's speak services. + """ + return SpeakRouter(self._config) + + @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.asyncspeak is deprecated. Use deepgram.speak.asyncrest instead.", + ) + def asyncspeak(self): + """ + DEPRECATED: deepgram.asyncspeak is deprecated. Use deepgram.speak.asyncrest instead. + """ + return self.Version(self._config, "asyncspeak") + + @property + def manage(self): + """ + Returns a ManageClient instance for managing Deepgram resources. + """ + return self.Version(self._config, "manage") + + @property + def asyncmanage(self): + """ + Returns an AsyncManageClient instance for managing Deepgram resources. + """ + return self.Version(self._config, "asyncmanage") + + @property + def auth(self): + """ + Returns an AuthRESTClient instance for managing short-lived tokens. + """ + return self.Version(self._config, "auth") + + @property + def asyncauth(self): + """ + Returns an AsyncAuthRESTClient instance for managing short-lived tokens. + """ + return self.Version(self._config, "asyncauth") + + @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.onprem is deprecated. Use deepgram.speak.selfhosted instead.", + ) + def onprem(self): + """ + DEPRECATED: deepgram.onprem is deprecated. Use deepgram.speak.selfhosted instead. + """ + return self.Version(self._config, "selfhosted") + + @property + def selfhosted(self): + """ + Returns an SelfHostedClient instance for interacting with Deepgram's on-premises API. + """ + return self.Version(self._config, "selfhosted") + + @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.asynconprem is deprecated. Use deepgram.speak.asyncselfhosted instead.", + ) + def asynconprem(self): + """ + DEPRECATED: deepgram.asynconprem is deprecated. Use deepgram.speak.asyncselfhosted instead. + """ + return self.Version(self._config, "asyncselfhosted") + + @property + def asyncselfhosted(self): + """ + Returns an AsyncSelfHostedClient instance for interacting with Deepgram's on-premises API. + """ + return self.Version(self._config, "asyncselfhosted") + + @property + def agent(self): + """ + Returns a Agent dot-notation router for interacting with Deepgram's speak services. + """ + return AgentRouter(self._config) + + # INTERNAL CLASSES + class Version: + """ + Represents a version of the Deepgram API. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _parent: str + + def __init__(self, config, parent: str): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + self._parent = parent + + # FUTURE VERSIONING: + # When v2 or v1.1beta1 or etc. This allows easy access to the latest version of the API. + # @property + # def latest(self): + # match self._parent: + # case "manage": + # return ManageClient(self._config) + # case "selfhosted": + # return SelfHostedClient(self._config) + # case _: + # raise DeepgramModuleError("Invalid parent") + + def v(self, version: str = ""): + # pylint: disable-msg=too-many-statements + """ + Returns a client for the specified version of the API. + """ + self._logger.debug("Version.v ENTER") + self._logger.info("version: %s", version) + if len(version) == 0: + self._logger.error("version is empty") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid module version") + + parent = "" + filename = "" + classname = "" + match self._parent: + case "manage": + parent = "manage" + filename = "client" + classname = "ManageClient" + case "asyncmanage": + parent = "manage" + filename = "async_client" + classname = "AsyncManageClient" + case "asyncspeak": + return AsyncSpeakRESTClient(self._config) + case "selfhosted": + parent = "selfhosted" + filename = "client" + classname = "SelfHostedClient" + case "asyncselfhosted": + parent = "selfhosted" + filename = "async_client" + classname = "AsyncSelfHostedClient" + case "auth": + parent = "auth" + filename = "client" + classname = "AuthRESTClient" + case "asyncauth": + parent = "auth" + filename = "async_client" + classname = "AsyncAuthRESTClient" + case _: + self._logger.error("parent unknown: %s", self._parent) + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid parent type") + + # create class path + path = f"deepgram.clients.{parent}.v{version}.{filename}" + self._logger.info("path: %s", path) + self._logger.info("classname: %s", classname) + + # import class + mod = import_module(path) + if mod is None: + self._logger.error("module path is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find package") + + my_class = getattr(mod, classname) + if my_class is None: + self._logger.error("my_class is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find class") + + # instantiate class + my_class_instance = my_class(self._config) + self._logger.notice("Version.v succeeded") + self._logger.debug("Version.v LEAVE") + return my_class_instance + + # pylint: enable-msg=too-many-statements diff --git a/deepgram/clients/__init__.py b/deepgram/clients/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0f573d2e78f9086ffb5017620998f2d286c31b8d --- /dev/null +++ b/deepgram/clients/__init__.py @@ -0,0 +1,381 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +# common +from .common import ( + TextSource, + BufferSource, + StreamSource, + FileSource, + UrlSource, +) +from .common import BaseResponse + +# common (shared between analze and prerecorded) +from .common import ( + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, +) + +# common (shared between listen rest and websocket) +from .common import ( + ModelInfo, + Hit, + Search, +) +from .common import ( + OpenResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) +from .common import ( + DeepgramError, + DeepgramTypeError, + DeepgramApiError, + DeepgramUnknownApiError, +) +from .errors import DeepgramModuleError + +from .listen_router import ListenRouter +from .read_router import ReadRouter +from .speak_router import SpeakRouter +from .agent_router import AgentRouter + +# listen +from .listen import LiveTranscriptionEvents + +## backward compat +from .prerecorded import ( + PreRecordedClient, + AsyncPreRecordedClient, +) +from .live import ( + LiveClient, + AsyncLiveClient, +) + +# speech-to-text rest +from .listen import ListenRESTClient, AsyncListenRESTClient + +## input +from .listen import ( + # common + # UrlSource, + # BufferSource, + # StreamSource, + # TextSource, + # FileSource, + # unique + PreRecordedStreamSource, + PrerecordedSource, + ListenRestSource, +) + +from .listen import ( + ListenRESTOptions, + PrerecordedOptions, +) + +## output +from .listen import ( + #### top level + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, + #### shared + # Average, + # Intent, + # Intents, + # IntentsInfo, + # Segment, + # SentimentInfo, + # Sentiment, + # Sentiments, + # SummaryInfo, + # Topic, + # Topics, + # TopicsInfo, + #### between rest and websocket + # ModelInfo, + # Alternative, + # Hit, + # Search, + # Channel, + # Word, + # unique + Entity, + ListenRESTMetadata, + Paragraph, + Paragraphs, + ListenRESTResults, + Sentence, + Summaries, + SummaryV1, + SummaryV2, + Translation, + Utterance, + Warning, + ListenRESTAlternative, + ListenRESTChannel, + ListenRESTWord, +) + + +# speech-to-text websocket +from .listen import ListenWebSocketClient, AsyncListenWebSocketClient + +## input +from .listen import ( + ListenWebSocketOptions, + LiveOptions, +) + +## output +from .listen import ( + #### top level + LiveResultResponse, + ListenWSMetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + #### common websocket response + # OpenResponse, + # CloseResponse, + # ErrorResponse, + # UnhandledResponse, + #### uniqye + ListenWSMetadata, + ListenWSWord, + ListenWSAlternative, + ListenWSChannel, +) + +## clients +from .listen import ( + ListenWebSocketClient, + AsyncListenWebSocketClient, +) + + +# read/analyze +from .analyze import ReadClient, AsyncReadClient +from .analyze import AnalyzeClient, AsyncAnalyzeClient +from .analyze import AnalyzeOptions +from .analyze import ( + # common + # UrlSource, + # TextSource, + # BufferSource, + # StreamSource, + # FileSource + # unique + AnalyzeStreamSource, + AnalyzeSource, +) +from .analyze import ( + #### top level + AsyncAnalyzeResponse, + SyncAnalyzeResponse, + AnalyzeResponse, + #### shared between analyze and pre-recorded + # Average, + # Intent, + # Intents, + # IntentsInfo, + # Segment, + # SentimentInfo, + # Sentiment, + # Sentiments, + # SummaryInfo, + # Topic, + # Topics, + # TopicsInfo, + #### unique + AnalyzeMetadata, + AnalyzeResults, + AnalyzeSummary, +) + +# text-to-speech +## text-to-speech REST +from .speak import ( + #### top level + SpeakRESTOptions, + SpeakOptions, + # common + # TextSource, + # BufferSource, + # StreamSource, + # FileSource, + # unique + SpeakSource, + SpeakRestSource, + SpeakRESTSource, +) + +from .speak import ( + SpeakClient, # backward compat + SpeakRESTClient, + AsyncSpeakRESTClient, +) + +from .speak import ( + SpeakResponse, # backward compat + SpeakRESTResponse, +) + +## text-to-speech WebSocket +from .speak import SpeakWebSocketEvents, SpeakWebSocketMessage + +from .speak import ( + SpeakWSOptions, +) + +from .speak import ( + SpeakWebSocketClient, + AsyncSpeakWebSocketClient, + SpeakWSClient, + AsyncSpeakWSClient, +) + +from .speak import ( + #### top level + SpeakWSMetadataResponse, + FlushedResponse, + ClearedResponse, + WarningResponse, + #### common websocket response + # OpenResponse, + # CloseResponse, + # UnhandledResponse, + # ErrorResponse, +) + +# manage +from .manage import ManageClient, AsyncManageClient +from .manage import ( + ProjectOptions, + KeyOptions, + ScopeOptions, + InviteOptions, + UsageRequestOptions, + UsageSummaryOptions, + UsageFieldsOptions, +) +from .manage import ( + #### top level + Message, + ProjectsResponse, + ModelResponse, + ModelsResponse, + MembersResponse, + KeyResponse, + KeysResponse, + ScopesResponse, + InvitesResponse, + UsageRequest, + UsageResponse, + UsageRequestsResponse, + UsageSummaryResponse, + UsageFieldsResponse, + BalancesResponse, + #### shared + Project, + STTDetails, + TTSMetadata, + TTSDetails, + Member, + Key, + Invite, + Config, + STTUsageDetails, + Callback, + TokenDetail, + SpeechSegment, + TTSUsageDetails, + STTTokens, + TTSTokens, + UsageSummaryResults, + Resolution, + UsageModel, + Balance, +) + +# auth +from .auth import AuthRESTClient, AsyncAuthRESTClient +from .auth import ( + GrantTokenResponse, +) + +# selfhosted +from .selfhosted import ( + OnPremClient, + AsyncOnPremClient, + SelfHostedClient, + AsyncSelfHostedClient, +) + +# agent +from .agent import AgentWebSocketEvents + +# websocket +from .agent import ( + AgentWebSocketClient, + AsyncAgentWebSocketClient, +) + +from .agent import ( + #### common websocket response + # OpenResponse, + # CloseResponse, + # ErrorResponse, + # UnhandledResponse, + #### unique + WelcomeResponse, + SettingsAppliedResponse, + ConversationTextResponse, + UserStartedSpeakingResponse, + AgentThinkingResponse, + FunctionCallRequest, + AgentStartedSpeakingResponse, + AgentAudioDoneResponse, + InjectionRefusedResponse, +) + +from .agent import ( + # top level + SettingsOptions, + UpdatePromptOptions, + UpdateSpeakOptions, + InjectAgentMessageOptions, + FunctionCallResponse, + AgentKeepAlive, + # sub level + Listen, + ListenProvider, + Speak, + SpeakProvider, + Header, + Item, + Properties, + Parameters, + Function, + Think, + ThinkProvider, + Agent, + Input, + Output, + Audio, + Endpoint, +) diff --git a/deepgram/clients/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8841e3c35a21918abbe7b22f79103ce91c4b6ddb Binary files /dev/null and b/deepgram/clients/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/__pycache__/agent_router.cpython-313.pyc b/deepgram/clients/__pycache__/agent_router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..411457c9cca60c838280e0aa333d927f2c1e2ec8 Binary files /dev/null and b/deepgram/clients/__pycache__/agent_router.cpython-313.pyc differ diff --git a/deepgram/clients/__pycache__/errors.cpython-313.pyc b/deepgram/clients/__pycache__/errors.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63f3a26f0049a6bf9e3659792d37181bc177df5d Binary files /dev/null and b/deepgram/clients/__pycache__/errors.cpython-313.pyc differ diff --git a/deepgram/clients/__pycache__/listen_router.cpython-313.pyc b/deepgram/clients/__pycache__/listen_router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b214eec2527eb941683c7db323a831b67b6f7451 Binary files /dev/null and b/deepgram/clients/__pycache__/listen_router.cpython-313.pyc differ diff --git a/deepgram/clients/__pycache__/read_router.cpython-313.pyc b/deepgram/clients/__pycache__/read_router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c2839c11ee17eb0318a0f522af3feab5398b732 Binary files /dev/null and b/deepgram/clients/__pycache__/read_router.cpython-313.pyc differ diff --git a/deepgram/clients/__pycache__/speak_router.cpython-313.pyc b/deepgram/clients/__pycache__/speak_router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87f12eb8b5aba4d9971ac010add214e22c3946f4 Binary files /dev/null and b/deepgram/clients/__pycache__/speak_router.cpython-313.pyc differ diff --git a/deepgram/clients/agent/__init__.py b/deepgram/clients/agent/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..63c598ef733d10a968c077a6651e8144e0d0c3a8 --- /dev/null +++ b/deepgram/clients/agent/__init__.py @@ -0,0 +1,56 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .enums import AgentWebSocketEvents + +# websocket +from .client import ( + AgentWebSocketClient, + AsyncAgentWebSocketClient, +) + +from .client import ( + #### common websocket response + OpenResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, + #### unique + WelcomeResponse, + SettingsAppliedResponse, + ConversationTextResponse, + UserStartedSpeakingResponse, + AgentThinkingResponse, + FunctionCallRequest, + AgentStartedSpeakingResponse, + AgentAudioDoneResponse, + InjectionRefusedResponse, +) + +from .client import ( + # top level + SettingsOptions, + UpdatePromptOptions, + UpdateSpeakOptions, + InjectAgentMessageOptions, + FunctionCallResponse, + AgentKeepAlive, + # sub level + Listen, + ListenProvider, + Speak, + SpeakProvider, + Header, + Item, + Properties, + Parameters, + Function, + Think, + ThinkProvider, + Agent, + Input, + Output, + Audio, + Endpoint, +) diff --git a/deepgram/clients/agent/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/agent/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fb022e8482b2683243b77a92f86e671290b5630 Binary files /dev/null and b/deepgram/clients/agent/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/agent/__pycache__/client.cpython-313.pyc b/deepgram/clients/agent/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..297949802fe5086407ed92cac072a6d3dd2eed38 Binary files /dev/null and b/deepgram/clients/agent/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/agent/__pycache__/enums.cpython-313.pyc b/deepgram/clients/agent/__pycache__/enums.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..754731b1750d3f9ab7b7388873ac96beb1f3d6ae Binary files /dev/null and b/deepgram/clients/agent/__pycache__/enums.cpython-313.pyc differ diff --git a/deepgram/clients/agent/client.py b/deepgram/clients/agent/client.py new file mode 100644 index 0000000000000000000000000000000000000000..6dbfd08fc4863bd2cc5230d321c305245567d297 --- /dev/null +++ b/deepgram/clients/agent/client.py @@ -0,0 +1,102 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +# websocket +from .v1 import ( + AgentWebSocketClient as LatestAgentWebSocketClient, + AsyncAgentWebSocketClient as LatestAsyncAgentWebSocketClient, +) + +from .v1 import ( + #### common websocket response + BaseResponse as LatestBaseResponse, + OpenResponse as LatestOpenResponse, + CloseResponse as LatestCloseResponse, + ErrorResponse as LatestErrorResponse, + UnhandledResponse as LatestUnhandledResponse, + #### unique + WelcomeResponse as LatestWelcomeResponse, + SettingsAppliedResponse as LatestSettingsAppliedResponse, + ConversationTextResponse as LatestConversationTextResponse, + UserStartedSpeakingResponse as LatestUserStartedSpeakingResponse, + AgentThinkingResponse as LatestAgentThinkingResponse, + FunctionCallRequest as LatestFunctionCallRequest, + AgentStartedSpeakingResponse as LatestAgentStartedSpeakingResponse, + AgentAudioDoneResponse as LatestAgentAudioDoneResponse, + InjectionRefusedResponse as LatestInjectionRefusedResponse, +) + +from .v1 import ( + # top level + SettingsOptions as LatestSettingsOptions, + UpdatePromptOptions as LatestUpdatePromptOptions, + UpdateSpeakOptions as LatestUpdateSpeakOptions, + InjectAgentMessageOptions as LatestInjectAgentMessageOptions, + FunctionCallResponse as LatestFunctionCallResponse, + AgentKeepAlive as LatestAgentKeepAlive, + # sub level + Listen as LatestListen, + ListenProvider as LatestListenProvider, + Speak as LatestSpeak, + SpeakProvider as LatestSpeakProvider, + Header as LatestHeader, + Item as LatestItem, + Properties as LatestProperties, + Parameters as LatestParameters, + Function as LatestFunction, + Think as LatestThink, + ThinkProvider as LatestThinkProvider, + Agent as LatestAgent, + Input as LatestInput, + Output as LatestOutput, + Audio as LatestAudio, + Endpoint as LatestEndpoint, +) + + +# The vX/client.py points to the current supported version in the SDK. +# Older versions are supported in the SDK for backwards compatibility. + +AgentWebSocketClient = LatestAgentWebSocketClient +AsyncAgentWebSocketClient = LatestAsyncAgentWebSocketClient + +OpenResponse = LatestOpenResponse +CloseResponse = LatestCloseResponse +ErrorResponse = LatestErrorResponse +UnhandledResponse = LatestUnhandledResponse + +WelcomeResponse = LatestWelcomeResponse +SettingsAppliedResponse = LatestSettingsAppliedResponse +ConversationTextResponse = LatestConversationTextResponse +UserStartedSpeakingResponse = LatestUserStartedSpeakingResponse +AgentThinkingResponse = LatestAgentThinkingResponse +FunctionCallRequest = LatestFunctionCallRequest +AgentStartedSpeakingResponse = LatestAgentStartedSpeakingResponse +AgentAudioDoneResponse = LatestAgentAudioDoneResponse +InjectionRefusedResponse = LatestInjectionRefusedResponse + + +SettingsOptions = LatestSettingsOptions +UpdatePromptOptions = LatestUpdatePromptOptions +UpdateSpeakOptions = LatestUpdateSpeakOptions +InjectAgentMessageOptions = LatestInjectAgentMessageOptions +FunctionCallResponse = LatestFunctionCallResponse +AgentKeepAlive = LatestAgentKeepAlive + +Listen = LatestListen +ListenProvider = LatestListenProvider +Speak = LatestSpeak +SpeakProvider = LatestSpeakProvider +Header = LatestHeader +Item = LatestItem +Properties = LatestProperties +Parameters = LatestParameters +Function = LatestFunction +Think = LatestThink +ThinkProvider = LatestThinkProvider +Agent = LatestAgent +Input = LatestInput +Output = LatestOutput +Audio = LatestAudio +Endpoint = LatestEndpoint \ No newline at end of file diff --git a/deepgram/clients/agent/enums.py b/deepgram/clients/agent/enums.py new file mode 100644 index 0000000000000000000000000000000000000000..e0928dd294ee93da7001f4d3a53dfbddb7c71e07 --- /dev/null +++ b/deepgram/clients/agent/enums.py @@ -0,0 +1,36 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from aenum import StrEnum + +# Constants mapping to events from the Deepgram API + + +class AgentWebSocketEvents(StrEnum): + """ + Enumerates the possible Agent API events that can be received from the Deepgram API + """ + + # server + Open: str = "Open" + Close: str = "Close" + AudioData: str = "AudioData" + Welcome: str = "Welcome" + SettingsApplied: str = "SettingsApplied" + ConversationText: str = "ConversationText" + UserStartedSpeaking: str = "UserStartedSpeaking" + AgentThinking: str = "AgentThinking" + FunctionCallRequest: str = "FunctionCallRequest" + AgentStartedSpeaking: str = "AgentStartedSpeaking" + AgentAudioDone: str = "AgentAudioDone" + Error: str = "Error" + Unhandled: str = "Unhandled" + + # client + Settings: str = "Settings" + UpdatePrompt: str = "UpdatePrompt" + UpdateSpeak: str = "UpdateSpeak" + InjectAgentMessage: str = "InjectAgentMessage" + InjectionRefused: str = "InjectionRefused" + AgentKeepAlive: str = "AgentKeepAlive" diff --git a/deepgram/clients/agent/v1/__init__.py b/deepgram/clients/agent/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8d48b80b5e769cd9a65a787a027c716b84c7a6d2 --- /dev/null +++ b/deepgram/clients/agent/v1/__init__.py @@ -0,0 +1,60 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +# common websocket +from ...common import ( + OpenResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) + +# websocket +from .websocket import AgentWebSocketClient, AsyncAgentWebSocketClient + +from .websocket import ( + #### common websocket response + BaseResponse, + OpenResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, + #### unique + WelcomeResponse, + SettingsAppliedResponse, + ConversationTextResponse, + UserStartedSpeakingResponse, + AgentThinkingResponse, + FunctionCallRequest, + AgentStartedSpeakingResponse, + AgentAudioDoneResponse, + InjectionRefusedResponse, +) + +from .websocket import ( + # top level + SettingsOptions, + UpdatePromptOptions, + UpdateSpeakOptions, + InjectAgentMessageOptions, + FunctionCallResponse, + AgentKeepAlive, + # sub level + Listen, + ListenProvider, + Speak, + SpeakProvider, + Header, + Item, + Properties, + Parameters, + Function, + Think, + ThinkProvider, + Agent, + Input, + Output, + Audio, + Endpoint, +) diff --git a/deepgram/clients/agent/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/agent/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..400e6897dbdaba326713ad23186c8c7b9e2d5874 Binary files /dev/null and b/deepgram/clients/agent/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/agent/v1/websocket/__init__.py b/deepgram/clients/agent/v1/websocket/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b1cec3f2848c642f31b7fbdcd462ea8c33a78817 --- /dev/null +++ b/deepgram/clients/agent/v1/websocket/__init__.py @@ -0,0 +1,51 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import AgentWebSocketClient +from .async_client import AsyncAgentWebSocketClient + +from .response import ( + #### common websocket response + BaseResponse, + OpenResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, + #### unique + WelcomeResponse, + SettingsAppliedResponse, + ConversationTextResponse, + UserStartedSpeakingResponse, + AgentThinkingResponse, + FunctionCallRequest, + AgentStartedSpeakingResponse, + AgentAudioDoneResponse, + InjectionRefusedResponse, +) +from .options import ( + # top level + SettingsOptions, + UpdatePromptOptions, + UpdateSpeakOptions, + InjectAgentMessageOptions, + FunctionCallResponse, + AgentKeepAlive, + # sub level + Listen, + ListenProvider, + Speak, + SpeakProvider, + Header, + Item, + Properties, + Parameters, + Function, + Think, + ThinkProvider, + Agent, + Input, + Output, + Audio, + Endpoint, +) diff --git a/deepgram/clients/agent/v1/websocket/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/agent/v1/websocket/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8315df7aa99a24355faad51f9fe30df8066f9bff Binary files /dev/null and b/deepgram/clients/agent/v1/websocket/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/agent/v1/websocket/__pycache__/async_client.cpython-313.pyc b/deepgram/clients/agent/v1/websocket/__pycache__/async_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb8022cc503b7a39e795e82c7fd59a31b03883f0 Binary files /dev/null and b/deepgram/clients/agent/v1/websocket/__pycache__/async_client.cpython-313.pyc differ diff --git a/deepgram/clients/agent/v1/websocket/__pycache__/client.cpython-313.pyc b/deepgram/clients/agent/v1/websocket/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6bc2d4b63331b33eb9bc5f969f7db0f75e53fc3 Binary files /dev/null and b/deepgram/clients/agent/v1/websocket/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/agent/v1/websocket/__pycache__/options.cpython-313.pyc b/deepgram/clients/agent/v1/websocket/__pycache__/options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..677e3e0853c7ddc3bca378ea99a91b51224fc116 Binary files /dev/null and b/deepgram/clients/agent/v1/websocket/__pycache__/options.cpython-313.pyc differ diff --git a/deepgram/clients/agent/v1/websocket/__pycache__/response.cpython-313.pyc b/deepgram/clients/agent/v1/websocket/__pycache__/response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e441fddd54c1fc1d197222ca9261842b60fc5bd Binary files /dev/null and b/deepgram/clients/agent/v1/websocket/__pycache__/response.cpython-313.pyc differ diff --git a/deepgram/clients/agent/v1/websocket/async_client.py b/deepgram/clients/agent/v1/websocket/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..4652446880bb03d16e67113abbee09763ca2ff90 --- /dev/null +++ b/deepgram/clients/agent/v1/websocket/async_client.py @@ -0,0 +1,688 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import asyncio +import json +import logging +from typing import Dict, Union, Optional, cast, Any, Callable +import threading + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import AgentWebSocketEvents +from ....common import AbstractAsyncWebSocketClient +from ....common import DeepgramError + +from .response import ( + OpenResponse, + WelcomeResponse, + SettingsAppliedResponse, + ConversationTextResponse, + UserStartedSpeakingResponse, + AgentThinkingResponse, + FunctionCallRequest, + AgentStartedSpeakingResponse, + AgentAudioDoneResponse, + InjectionRefusedResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) +from .options import ( + SettingsOptions, + UpdatePromptOptions, + UpdateSpeakOptions, + InjectAgentMessageOptions, + FunctionCallResponse, + AgentKeepAlive, +) + +from .....audio.speaker import ( + Speaker, + RATE as SPEAKER_RATE, + CHANNELS as SPEAKER_CHANNELS, + PLAYBACK_DELTA as SPEAKER_PLAYBACK_DELTA, +) +from .....audio.microphone import ( + Microphone, + RATE as MICROPHONE_RATE, + CHANNELS as MICROPHONE_CHANNELS, +) + +ONE_SECOND = 1 +HALF_SECOND = 0.5 +DEEPGRAM_INTERVAL = 5 + + +class AsyncAgentWebSocketClient( + AbstractAsyncWebSocketClient +): # pylint: disable=too-many-instance-attributes + """ + Client for interacting with Deepgram's live transcription services over WebSockets. + + This class provides methods to establish a WebSocket connection for live transcription and handle real-time transcription events. + + Args: + config (DeepgramClientOptions): all the options for the client. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + _event_handlers: Dict[AgentWebSocketEvents, list] + + _keep_alive_thread: Union[asyncio.Task, None] + + _kwargs: Optional[Dict] = None + _addons: Optional[Dict] = None + # note the distinction here. We can't use _config because it's already used in the parent + _settings: Optional[SettingsOptions] = None + _headers: Optional[Dict] = None + + _speaker_created: bool = False + _speaker: Optional[Speaker] = None + _microphone_created: bool = False + _microphone: Optional[Microphone] = None + + def __init__(self, config: DeepgramClientOptions): + if config is None: + raise DeepgramError("Config is required") + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + + # needs to be "wss://agent.deepgram.com/agent" + self._endpoint = "v1/agent/converse" + + # override the endpoint since it needs to be "wss://agent.deepgram.com/agent" + self._config.url = "agent.deepgram.com" + self._keep_alive_thread = None + + # init handlers + self._event_handlers = { + event: [] for event in AgentWebSocketEvents.__members__.values() + } + + if self._config.options.get("microphone_record") == "true": + self._logger.info("microphone_record is enabled") + rate = self._config.options.get("microphone_record_rate", MICROPHONE_RATE) + channels = self._config.options.get( + "microphone_record_channels", MICROPHONE_CHANNELS + ) + device_index = self._config.options.get("microphone_record_device_index") + + self._logger.debug("rate: %s", rate) + self._logger.debug("channels: %s", channels) + if device_index is not None: + self._logger.debug("device_index: %s", device_index) + + self._microphone_created = True + + if device_index is not None: + self._microphone = Microphone( + rate=rate, + channels=channels, + verbose=self._config.verbose, + input_device_index=device_index, + ) + else: + self._microphone = Microphone( + rate=rate, + channels=channels, + verbose=self._config.verbose, + ) + + if self._config.options.get("speaker_playback") == "true": + self._logger.info("speaker_playback is enabled") + rate = self._config.options.get("speaker_playback_rate", SPEAKER_RATE) + channels = self._config.options.get( + "speaker_playback_channels", SPEAKER_CHANNELS + ) + playback_delta_in_ms = self._config.options.get( + "speaker_playback_delta_in_ms", SPEAKER_PLAYBACK_DELTA + ) + device_index = self._config.options.get("speaker_playback_device_index") + + self._logger.debug("rate: %s", rate) + self._logger.debug("channels: %s", channels) + + self._speaker_created = True + + if device_index is not None: + self._logger.debug("device_index: %s", device_index) + + self._speaker = Speaker( + rate=rate, + channels=channels, + last_play_delta_in_ms=playback_delta_in_ms, + verbose=self._config.verbose, + output_device_index=device_index, + microphone=self._microphone, + ) + else: + self._speaker = Speaker( + rate=rate, + channels=channels, + last_play_delta_in_ms=playback_delta_in_ms, + verbose=self._config.verbose, + microphone=self._microphone, + ) + # call the parent constructor + super().__init__(self._config, self._endpoint) + + # pylint: disable=too-many-branches,too-many-statements + async def start( + self, + options: Optional[SettingsOptions] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + members: Optional[Dict] = None, + **kwargs, + ) -> bool: + """ + Starts the WebSocket connection for agent API. + """ + self._logger.debug("AsyncAgentWebSocketClient.start ENTER") + self._logger.info("settings: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + self._logger.info("members: %s", members) + self._logger.info("kwargs: %s", kwargs) + + if isinstance(options, SettingsOptions) and not options.check(): + self._logger.error("settings.check failed") + self._logger.debug("AsyncAgentWebSocketClient.start LEAVE") + raise DeepgramError("Fatal agent settings error") + + self._addons = addons + self._headers = headers + + # add "members" as members of the class + if members is not None: + self.__dict__.update(members) + + # set kwargs as members of the class + if kwargs is not None: + self._kwargs = kwargs + else: + self._kwargs = {} + + if isinstance(options, SettingsOptions): + self._logger.info("options is class") + self._settings = options + elif isinstance(options, dict): + self._logger.info("options is dict") + self._settings = SettingsOptions.from_dict(options) + elif isinstance(options, str): + self._logger.info("options is json") + self._settings = SettingsOptions.from_json(options) + else: + raise DeepgramError("Invalid options type") + + if self._settings.agent.listen.provider.keyterms is not None and self._settings.agent.listen.provider.model is not None and not self._settings.agent.listen.provider.model.startswith("nova-3"): + raise DeepgramError("Keyterms are only supported for nova-3 models") + + try: + # speaker substitutes the listening thread + if self._speaker is not None: + self._logger.notice("passing speaker to delegate_listening") + super().delegate_listening(self._speaker) + + # call parent start + if ( + await super().start( + {}, + self._addons, + self._headers, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + is False + ): + self._logger.error("AsyncAgentWebSocketClient.start failed") + self._logger.debug("AsyncAgentWebSocketClient.start LEAVE") + return False + + if self._speaker is not None: + self._logger.notice("speaker is delegate_listening. Starting speaker") + self._speaker.start() + + if self._speaker is not None and self._microphone is not None: + self._logger.notice( + "speaker is delegate_listening. Starting microphone" + ) + self._microphone.set_callback(self.send) + self._microphone.start() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # keepalive thread + if self._config.is_keep_alive_enabled(): + self._logger.notice("keepalive is enabled") + self._keep_alive_thread = asyncio.create_task(self._keep_alive()) + else: + self._logger.notice("keepalive is disabled") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # send the configurationsetting message + self._logger.notice("Sending Settings...") + ret_send_cs = await self.send(str(self._settings)) + if not ret_send_cs: + self._logger.error("Settings failed") + + err_error: ErrorResponse = ErrorResponse( + "Exception in AsyncAgentWebSocketClient.start", + "Settings failed to send", + "Exception", + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.debug("AgentWebSocketClient.start LEAVE") + return False + + self._logger.notice("start succeeded") + self._logger.debug("AsyncAgentWebSocketClient.start LEAVE") + return True + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "WebSocketException in AsyncAgentWebSocketClient.start: %s", e + ) + self._logger.debug("AsyncAgentWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect") is True: + raise e + return False + + # pylint: enable=too-many-branches,too-many-statements + + def on(self, event: AgentWebSocketEvents, handler: Callable) -> None: + """ + Registers event handlers for specific events. + """ + self._logger.info("event subscribed: %s", event) + if event in AgentWebSocketEvents.__members__.values() and callable(handler): + self._event_handlers[event].append(handler) + + async def _emit(self, event: AgentWebSocketEvents, *args, **kwargs) -> None: + """ + Emits events to the registered event handlers. + """ + self._logger.debug("AsyncAgentWebSocketClient._emit ENTER") + self._logger.debug("callback handlers for: %s", event) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("callback handlers for: %s", event) + tasks = [] + for handler in self._event_handlers[event]: + task = asyncio.create_task(handler(self, *args, **kwargs)) + tasks.append(task) + + if tasks: + self._logger.debug("waiting for tasks to finish...") + await asyncio.gather(*tasks, return_exceptions=True) + tasks.clear() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("AsyncAgentWebSocketClient._emit LEAVE") + + # pylint: disable=too-many-locals,too-many-statements + async def _process_text(self, message: str) -> None: + """ + Processes messages received over the WebSocket connection. + """ + self._logger.debug("AsyncAgentWebSocketClient._process_text ENTER") + + try: + self._logger.debug("Text data received") + if len(message) == 0: + self._logger.debug("message is empty") + self._logger.debug("AsyncAgentWebSocketClient._process_text LEAVE") + return + + data = json.loads(message) + response_type = data.get("type") + self._logger.debug("response_type: %s, data: %s", response_type, data) + + match response_type: + case AgentWebSocketEvents.Open: + open_result: OpenResponse = OpenResponse.from_json(message) + self._logger.verbose("OpenResponse: %s", open_result) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Open), + open=open_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.Welcome: + welcome_result: WelcomeResponse = WelcomeResponse.from_json(message) + self._logger.verbose("WelcomeResponse: %s", welcome_result) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Welcome), + welcome=welcome_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.SettingsApplied: + settings_applied_result: SettingsAppliedResponse = ( + SettingsAppliedResponse.from_json(message) + ) + self._logger.verbose( + "SettingsAppliedResponse: %s", settings_applied_result + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.SettingsApplied), + settings_applied=settings_applied_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.ConversationText: + conversation_text_result: ConversationTextResponse = ( + ConversationTextResponse.from_json(message) + ) + self._logger.verbose( + "ConversationTextResponse: %s", conversation_text_result + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.ConversationText), + conversation_text=conversation_text_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.UserStartedSpeaking: + user_started_speaking_result: UserStartedSpeakingResponse = ( + UserStartedSpeakingResponse.from_json(message) + ) + self._logger.verbose( + "UserStartedSpeakingResponse: %s", user_started_speaking_result + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.UserStartedSpeaking), + user_started_speaking=user_started_speaking_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.AgentThinking: + agent_thinking_result: AgentThinkingResponse = ( + AgentThinkingResponse.from_json(message) + ) + self._logger.verbose( + "AgentThinkingResponse: %s", agent_thinking_result + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.AgentThinking), + agent_thinking=agent_thinking_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.FunctionCallRequest: + function_call_request_result: FunctionCallRequest = ( + FunctionCallRequest.from_json(message) + ) + self._logger.verbose( + "FunctionCallRequest: %s", function_call_request_result + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.FunctionCallRequest), + function_call_request=function_call_request_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.AgentStartedSpeaking: + agent_started_speaking_result: AgentStartedSpeakingResponse = ( + AgentStartedSpeakingResponse.from_json(message) + ) + self._logger.verbose( + "AgentStartedSpeakingResponse: %s", + agent_started_speaking_result, + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.AgentStartedSpeaking), + agent_started_speaking=agent_started_speaking_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.AgentAudioDone: + agent_audio_done_result: AgentAudioDoneResponse = ( + AgentAudioDoneResponse.from_json(message) + ) + self._logger.verbose( + "AgentAudioDoneResponse: %s", agent_audio_done_result + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.AgentAudioDone), + agent_audio_done=agent_audio_done_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.InjectionRefused: + injection_refused_result: InjectionRefusedResponse = ( + InjectionRefusedResponse.from_json(message) + ) + self._logger.verbose( + "InjectionRefused: %s", injection_refused_result + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.InjectionRefused), + injection_refused=injection_refused_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.Close: + close_result: CloseResponse = CloseResponse.from_json(message) + self._logger.verbose("CloseResponse: %s", close_result) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Close), + close=close_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.Error: + err_error: ErrorResponse = ErrorResponse.from_json(message) + self._logger.verbose("ErrorResponse: %s", err_error) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case _: + self._logger.warning( + "Unknown Message: response_type: %s, data: %s", + response_type, + data, + ) + unhandled_error: UnhandledResponse = UnhandledResponse( + type=AgentWebSocketEvents(AgentWebSocketEvents.Unhandled), + raw=message, + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Unhandled), + unhandled=unhandled_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_text Succeeded") + self._logger.debug("AsyncAgentWebSocketClient._process_text LEAVE") + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AsyncAgentWebSocketClient._process_text: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AsyncAgentWebSocketClient._process_text", + f"{e}", + "Exception", + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await super()._signal_exit() + + self._logger.debug("AsyncAgentWebSocketClient._process_text LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-locals,too-many-statements + + async def _process_binary(self, message: bytes) -> None: + self._logger.debug("AsyncAgentWebSocketClient._process_binary ENTER") + self._logger.debug("Binary data received") + + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.AudioData), + data=message, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_binary Succeeded") + self._logger.debug("AsyncAgentWebSocketClient._process_binary LEAVE") + + # pylint: disable=too-many-return-statements + async def _keep_alive(self) -> None: + """ + Sends keepalive messages to the WebSocket connection. + """ + self._logger.debug("AsyncAgentWebSocketClient._keep_alive ENTER") + + counter = 0 + while True: + try: + counter += 1 + await asyncio.sleep(ONE_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_keep_alive exiting gracefully") + self._logger.debug("AsyncAgentWebSocketClient._keep_alive LEAVE") + return + + # deepgram keepalive + if counter % DEEPGRAM_INTERVAL == 0: + await self.keep_alive() + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AsyncAgentWebSocketClient._keep_alive: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AsyncAgentWebSocketClient._keep_alive", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in AsyncAgentWebSocketClient._keep_alive: %s", str(e) + ) + await self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await super()._signal_exit() + + self._logger.debug("AsyncAgentWebSocketClient._keep_alive LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + async def keep_alive(self) -> bool: + """ + Sends a KeepAlive message + """ + self._logger.spam("AsyncAgentWebSocketClient.keep_alive ENTER") + + self._logger.notice("Sending KeepAlive...") + ret = await self.send(json.dumps({"type": "KeepAlive"})) + + if not ret: + self._logger.error("keep_alive failed") + self._logger.spam("AsyncAgentWebSocketClient.keep_alive LEAVE") + return False + + self._logger.notice("keep_alive succeeded") + self._logger.spam("AsyncAgentWebSocketClient.keep_alive LEAVE") + + return True + + async def _close_message(self) -> bool: + # TODO: No known API close message # pylint: disable=fixme + # return await self.send(json.dumps({"type": "Close"})) + return True + + async def finish(self) -> bool: + """ + Closes the WebSocket connection gracefully. + """ + self._logger.debug("AsyncAgentWebSocketClient.finish ENTER") + + # stop the threads + self._logger.verbose("cancelling tasks...") + try: + # call parent finish + if await super().finish() is False: + self._logger.error("AsyncAgentWebSocketClient.finish failed") + + if self._microphone is not None and self._microphone_created: + self._microphone.finish() + self._microphone_created = False + + if self._speaker is not None and self._speaker_created: + self._speaker.finish() + self._speaker_created = False + + # Before cancelling, check if the tasks were created + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + tasks = [] + if self._keep_alive_thread is not None: + self._keep_alive_thread.cancel() + tasks.append(self._keep_alive_thread) + self._logger.notice("processing _keep_alive_thread cancel...") + + # Use asyncio.gather to wait for tasks to be cancelled + # Prevent indefinite waiting by setting a timeout + await asyncio.wait_for(asyncio.gather(*tasks), timeout=10) + self._logger.notice("threads joined") + + self._speaker = None + self._microphone = None + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("finish succeeded") + self._logger.spam("AsyncAgentWebSocketClient.finish LEAVE") + return True + + except asyncio.CancelledError as e: + self._logger.error("tasks cancelled error: %s", e) + self._logger.debug("AsyncAgentWebSocketClient.finish LEAVE") + return False + + except asyncio.TimeoutError as e: + self._logger.error("tasks cancellation timed out: %s", e) + self._logger.debug("AsyncAgentWebSocketClient.finish LEAVE") + return False diff --git a/deepgram/clients/agent/v1/websocket/client.py b/deepgram/clients/agent/v1/websocket/client.py new file mode 100644 index 0000000000000000000000000000000000000000..fae7830b9b5daebb83e96e150a4b30cc295b822d --- /dev/null +++ b/deepgram/clients/agent/v1/websocket/client.py @@ -0,0 +1,677 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import json +import logging +from typing import Dict, Union, Optional, cast, Any, Callable +import threading +import time + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import AgentWebSocketEvents +from ....common import AbstractSyncWebSocketClient +from ....common import DeepgramError + +from .response import ( + OpenResponse, + WelcomeResponse, + SettingsAppliedResponse, + ConversationTextResponse, + UserStartedSpeakingResponse, + AgentThinkingResponse, + FunctionCallRequest, + AgentStartedSpeakingResponse, + AgentAudioDoneResponse, + InjectionRefusedResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) +from .options import ( + SettingsOptions, + UpdatePromptOptions, + UpdateSpeakOptions, + InjectAgentMessageOptions, + FunctionCallResponse, + AgentKeepAlive, +) + +from .....audio.speaker import ( + Speaker, + RATE as SPEAKER_RATE, + CHANNELS as SPEAKER_CHANNELS, + PLAYBACK_DELTA as SPEAKER_PLAYBACK_DELTA, +) +from .....audio.microphone import ( + Microphone, + RATE as MICROPHONE_RATE, + CHANNELS as MICROPHONE_CHANNELS, +) + +ONE_SECOND = 1 +HALF_SECOND = 0.5 +DEEPGRAM_INTERVAL = 5 + + +class AgentWebSocketClient( + AbstractSyncWebSocketClient +): # pylint: disable=too-many-instance-attributes + """ + Client for interacting with Deepgram's live transcription services over WebSockets. + + This class provides methods to establish a WebSocket connection for live transcription and handle real-time transcription events. + + Args: + config (DeepgramClientOptions): all the options for the client. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + _event_handlers: Dict[AgentWebSocketEvents, list] + + _keep_alive_thread: Union[threading.Thread, None] + + _kwargs: Optional[Dict] = None + _addons: Optional[Dict] = None + # note the distinction here. We can't use _config because it's already used in the parent + _settings: Optional[SettingsOptions] = None + _headers: Optional[Dict] = None + + _speaker_created: bool = False + _speaker: Optional[Speaker] = None + _microphone_created: bool = False + _microphone: Optional[Microphone] = None + + def __init__(self, config: DeepgramClientOptions): + if config is None: + raise DeepgramError("Config is required") + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + + # needs to be "wss://agent.deepgram.com/agent" + self._endpoint = "v1/agent/converse" + + # override the endpoint since it needs to be "wss://agent.deepgram.com/agent" + self._config.url = "agent.deepgram.com" + + self._keep_alive_thread = None + + # init handlers + self._event_handlers = { + event: [] for event in AgentWebSocketEvents.__members__.values() + } + + if self._config.options.get("microphone_record") == "true": + self._logger.info("microphone_record is enabled") + rate = self._config.options.get("microphone_record_rate", MICROPHONE_RATE) + channels = self._config.options.get( + "microphone_record_channels", MICROPHONE_CHANNELS + ) + device_index = self._config.options.get("microphone_record_device_index") + + self._logger.debug("rate: %s", rate) + self._logger.debug("channels: %s", channels) + + self._microphone_created = True + + if device_index is not None: + self._logger.debug("device_index: %s", device_index) + self._microphone = Microphone( + rate=rate, + channels=channels, + verbose=self._config.verbose, + input_device_index=device_index, + ) + else: + self._microphone = Microphone( + rate=rate, + channels=channels, + verbose=self._config.verbose, + ) + + if self._config.options.get("speaker_playback") == "true": + self._logger.info("speaker_playback is enabled") + rate = self._config.options.get("speaker_playback_rate", SPEAKER_RATE) + channels = self._config.options.get( + "speaker_playback_channels", SPEAKER_CHANNELS + ) + playback_delta_in_ms = self._config.options.get( + "speaker_playback_delta_in_ms", SPEAKER_PLAYBACK_DELTA + ) + device_index = self._config.options.get("speaker_playback_device_index") + + self._logger.debug("rate: %s", rate) + self._logger.debug("channels: %s", channels) + + self._speaker_created = True + + if device_index is not None: + self._logger.debug("device_index: %s", device_index) + + self._speaker = Speaker( + rate=rate, + channels=channels, + last_play_delta_in_ms=playback_delta_in_ms, + verbose=self._config.verbose, + output_device_index=device_index, + microphone=self._microphone, + ) + else: + self._speaker = Speaker( + rate=rate, + channels=channels, + last_play_delta_in_ms=playback_delta_in_ms, + verbose=self._config.verbose, + microphone=self._microphone, + ) + + # call the parent constructor + super().__init__(self._config, self._endpoint) + + # pylint: disable=too-many-statements,too-many-branches + def start( + self, + options: Optional[SettingsOptions] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + members: Optional[Dict] = None, + **kwargs, + ) -> bool: + """ + Starts the WebSocket connection for agent API. + """ + self._logger.debug("AgentWebSocketClient.start ENTER") + self._logger.info("settings: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + self._logger.info("members: %s", members) + self._logger.info("kwargs: %s", kwargs) + + if isinstance(options, SettingsOptions) and not options.check(): + self._logger.error("settings.check failed") + self._logger.debug("AgentWebSocketClient.start LEAVE") + raise DeepgramError("Fatal agent settings error") + + self._addons = addons + self._headers = headers + + # add "members" as members of the class + if members is not None: + self.__dict__.update(members) + + # set kwargs as members of the class + if kwargs is not None: + self._kwargs = kwargs + else: + self._kwargs = {} + + if isinstance(options, SettingsOptions): + self._logger.info("options is class") + self._settings = options + elif isinstance(options, dict): + self._logger.info("options is dict") + self._settings = SettingsOptions.from_dict(options) + elif isinstance(options, str): + self._logger.info("options is json") + self._settings = SettingsOptions.from_json(options) + else: + raise DeepgramError("Invalid options type") + + if ( + self._settings.agent.listen.provider + and self._settings.agent.listen.provider.keyterms is not None + and self._settings.agent.listen.provider.model is not None + and not self._settings.agent.listen.provider.model.startswith("nova-3") + ): + raise DeepgramError("Keyterms are only supported for nova-3 models") + + try: + # speaker substitutes the listening thread + if self._speaker is not None: + self._logger.notice("passing speaker to delegate_listening") + super().delegate_listening(self._speaker) + + # call parent start + if ( + super().start( + {}, + self._addons, + self._headers, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + is False + ): + self._logger.error("AgentWebSocketClient.start failed") + self._logger.debug("AgentWebSocketClient.start LEAVE") + return False + + if self._speaker is not None: + self._logger.notice("speaker is delegate_listening. Starting speaker") + self._speaker.start() + + if self._speaker is not None and self._microphone is not None: + self._logger.notice( + "speaker is delegate_listening. Starting microphone" + ) + self._microphone.set_callback(self.send) + self._microphone.start() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # keepalive thread + if self._config.is_keep_alive_enabled(): + self._logger.notice("keepalive is enabled") + self._keep_alive_thread = threading.Thread(target=self._keep_alive) + self._keep_alive_thread.start() + else: + self._logger.notice("keepalive is disabled") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # send the Settings message + self._logger.notice("Sending Settings...") + ret_send_cs = self.send(str(self._settings)) + if not ret_send_cs: + self._logger.error("Settings failed") + + err_error: ErrorResponse = ErrorResponse( + "Exception in AgentWebSocketClient.start", + "Settings failed to send", + "Exception", + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.debug("AgentWebSocketClient.start LEAVE") + return False + + self._logger.notice("start succeeded") + self._logger.debug("AgentWebSocketClient.start LEAVE") + return True + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "WebSocketException in AgentWebSocketClient.start: %s", e + ) + self._logger.debug("AgentWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect") is True: + raise e + return False + + # pylint: enable=too-many-statements,too-many-branches + + def on(self, event: AgentWebSocketEvents, handler: Callable) -> None: + """ + Registers event handlers for specific events. + """ + self._logger.info("event subscribed: %s", event) + if event in AgentWebSocketEvents.__members__.values() and callable(handler): + self._event_handlers[event].append(handler) + + def _emit(self, event: AgentWebSocketEvents, *args, **kwargs) -> None: + """ + Emits events to the registered event handlers. + """ + self._logger.debug("AgentWebSocketClient._emit ENTER") + self._logger.debug("callback handlers for: %s", event) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("callback handlers for: %s", event) + for handler in self._event_handlers[event]: + handler(self, *args, **kwargs) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("AgentWebSocketClient._emit LEAVE") + + # pylint: disable=too-many-return-statements,too-many-statements,too-many-locals,too-many-branches + def _process_text(self, message: str) -> None: + """ + Processes messages received over the WebSocket connection. + """ + self._logger.debug("AgentWebSocketClient._process_text ENTER") + + try: + self._logger.debug("Text data received") + if len(message) == 0: + self._logger.debug("message is empty") + self._logger.debug("AgentWebSocketClient._process_text LEAVE") + return + + data = json.loads(message) + response_type = data.get("type") + self._logger.debug("response_type: %s, data: %s", response_type, data) + + match response_type: + case AgentWebSocketEvents.Open: + open_result: OpenResponse = OpenResponse.from_json(message) + self._logger.verbose("OpenResponse: %s", open_result) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Open), + open=open_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.Welcome: + welcome_result: WelcomeResponse = WelcomeResponse.from_json(message) + self._logger.verbose("WelcomeResponse: %s", welcome_result) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Welcome), + welcome=welcome_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.SettingsApplied: + settings_applied_result: SettingsAppliedResponse = ( + SettingsAppliedResponse.from_json(message) + ) + self._logger.verbose( + "SettingsAppliedResponse: %s", settings_applied_result + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.SettingsApplied), + settings_applied=settings_applied_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.ConversationText: + conversation_text_result: ConversationTextResponse = ( + ConversationTextResponse.from_json(message) + ) + self._logger.verbose( + "ConversationTextResponse: %s", conversation_text_result + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.ConversationText), + conversation_text=conversation_text_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.UserStartedSpeaking: + user_started_speaking_result: UserStartedSpeakingResponse = ( + UserStartedSpeakingResponse.from_json(message) + ) + self._logger.verbose( + "UserStartedSpeakingResponse: %s", user_started_speaking_result + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.UserStartedSpeaking), + user_started_speaking=user_started_speaking_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.AgentThinking: + agent_thinking_result: AgentThinkingResponse = ( + AgentThinkingResponse.from_json(message) + ) + self._logger.verbose( + "AgentThinkingResponse: %s", agent_thinking_result + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.AgentThinking), + agent_thinking=agent_thinking_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.FunctionCallRequest: + function_call_request_result: FunctionCallRequest = ( + FunctionCallRequest.from_json(message) + ) + self._logger.verbose( + "FunctionCallRequest: %s", function_call_request_result + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.FunctionCallRequest), + function_call_request=function_call_request_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.AgentStartedSpeaking: + agent_started_speaking_result: AgentStartedSpeakingResponse = ( + AgentStartedSpeakingResponse.from_json(message) + ) + self._logger.verbose( + "AgentStartedSpeakingResponse: %s", + agent_started_speaking_result, + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.AgentStartedSpeaking), + agent_started_speaking=agent_started_speaking_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.AgentAudioDone: + agent_audio_done_result: AgentAudioDoneResponse = ( + AgentAudioDoneResponse.from_json(message) + ) + self._logger.verbose( + "AgentAudioDoneResponse: %s", agent_audio_done_result + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.AgentAudioDone), + agent_audio_done=agent_audio_done_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.InjectionRefused: + injection_refused_result: InjectionRefusedResponse = ( + InjectionRefusedResponse.from_json(message) + ) + self._logger.verbose( + "InjectionRefused: %s", injection_refused_result + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.InjectionRefused), + injection_refused=injection_refused_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.Close: + close_result: CloseResponse = CloseResponse.from_json(message) + self._logger.verbose("CloseResponse: %s", close_result) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Close), + close=close_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case AgentWebSocketEvents.Error: + err_error: ErrorResponse = ErrorResponse.from_json(message) + self._logger.verbose("ErrorResponse: %s", err_error) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case _: + self._logger.warning( + "Unknown Message: response_type: %s, data: %s", + response_type, + data, + ) + unhandled_error: UnhandledResponse = UnhandledResponse( + type=AgentWebSocketEvents(AgentWebSocketEvents.Unhandled), + raw=message, + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Unhandled), + unhandled=unhandled_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_text Succeeded") + self._logger.debug("SpeakStreamClient._process_text LEAVE") + + except Exception as e: # pylint: disable=broad-except + self._logger.error("Exception in AgentWebSocketClient._process_text: %s", e) + e_error: ErrorResponse = ErrorResponse( + "Exception in AgentWebSocketClient._process_text", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in AgentWebSocketClient._process_text: %s", str(e) + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + super()._signal_exit() + + self._logger.debug("AgentWebSocketClient._process_text LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-return-statements,too-many-statements + + def _process_binary(self, message: bytes) -> None: + self._logger.debug("AgentWebSocketClient._process_binary ENTER") + self._logger.debug("Binary data received") + + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.AudioData), + data=message, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_binary Succeeded") + self._logger.debug("AgentWebSocketClient._process_binary LEAVE") + + # pylint: disable=too-many-return-statements + def _keep_alive(self) -> None: + """ + Sends keepalive messages to the WebSocket connection. + """ + self._logger.debug("AgentWebSocketClient._keep_alive ENTER") + + counter = 0 + while True: + try: + counter += 1 + self._exit_event.wait(timeout=ONE_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_keep_alive exiting gracefully") + self._logger.debug("AgentWebSocketClient._keep_alive LEAVE") + return + + # deepgram keepalive + if counter % DEEPGRAM_INTERVAL == 0: + self.keep_alive() + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AgentWebSocketClient._keep_alive: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AgentWebSocketClient._keep_alive", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in AgentWebSocketClient._keep_alive: %s", str(e) + ) + self._emit( + AgentWebSocketEvents(AgentWebSocketEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + super()._signal_exit() + + self._logger.debug("AgentWebSocketClient._keep_alive LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + def keep_alive(self) -> bool: + """ + Sends a KeepAlive message + """ + self._logger.spam("AgentWebSocketClient.keep_alive ENTER") + + self._logger.notice("Sending KeepAlive...") + ret = self.send(json.dumps({"type": "KeepAlive"})) + + if not ret: + self._logger.error("keep_alive failed") + self._logger.spam("AgentWebSocketClient.keep_alive LEAVE") + return False + + self._logger.notice("keep_alive succeeded") + self._logger.spam("AgentWebSocketClient.keep_alive LEAVE") + + return True + + def _close_message(self) -> bool: + # TODO: No known API close message # pylint: disable=fixme + # return self.send(json.dumps({"type": "Close"})) + return True + + # closes the WebSocket connection gracefully + def finish(self) -> bool: + """ + Closes the WebSocket connection gracefully. + """ + self._logger.spam("AgentWebSocketClient.finish ENTER") + + # call parent finish + if super().finish() is False: + self._logger.error("AgentWebSocketClient.finish failed") + + if self._microphone is not None and self._microphone_created: + self._microphone.finish() + self._microphone_created = False + + if self._speaker is not None and self._speaker_created: + self._speaker.finish() + self._speaker_created = False + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # stop the threads + self._logger.verbose("cancelling tasks...") + if self._keep_alive_thread is not None: + self._keep_alive_thread.join() + self._keep_alive_thread = None + self._logger.notice("processing _keep_alive_thread thread joined") + + if self._listen_thread is not None: + self._listen_thread.join() + self._listen_thread = None + self._logger.notice("listening thread joined") + + self._speaker = None + self._microphone = None + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("finish succeeded") + self._logger.spam("AgentWebSocketClient.finish LEAVE") + return True diff --git a/deepgram/clients/agent/v1/websocket/options.py b/deepgram/clients/agent/v1/websocket/options.py new file mode 100644 index 0000000000000000000000000000000000000000..8ecea60ce61b1915d4da4f9eab4e0903d6b96c5c --- /dev/null +++ b/deepgram/clients/agent/v1/websocket/options.py @@ -0,0 +1,453 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Union, Any, Tuple +import logging + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from deepgram.utils import verboselogs + +from ...enums import AgentWebSocketEvents +from ....common import BaseResponse + + +# ConfigurationSettings + + +@dataclass +class Header(BaseResponse): + """ + This class defines a single key/value pair for a header. + """ + + key: str + value: str + + +@dataclass +class Item(BaseResponse): + """ + This class defines a single item in a list of items. + """ + + type: str + description: str + + +@dataclass +class Properties(BaseResponse): + """ + This class defines the properties which is just a list of items. + """ + + item: Item + + def __getitem__(self, key): + _dict = self.to_dict() + if "item" in _dict: + _dict["item"] = [Item.from_dict(item) for item in _dict["item"]] + return _dict[key] + + +@dataclass +class Parameters(BaseResponse): + """ + This class defines the parameters for a function. + """ + + type: str + properties: Properties + required: List[str] + + def __getitem__(self, key): + _dict = self.to_dict() + if "properties" in _dict: + _dict["properties"] = _dict["properties"].copy() + return _dict[key] + + +@dataclass +class Endpoint(BaseResponse): + """ + Define a custom endpoint for the agent. + """ + + method: Optional[str] = field(default="POST") + url: str = field(default="") + headers: Optional[List[Header]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "headers" in _dict: + _dict["headers"] = [ + Header.from_dict(headers) for headers in _dict["headers"] + ] + return _dict[key] + + +@dataclass +class Function(BaseResponse): + """ + This class defines a function for the Think model. + """ + + name: str + description: str + url: str + method: str + headers: Optional[List[Header]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + parameters: Optional[Parameters] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + endpoint: Optional[Endpoint] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "parameters" in _dict and isinstance(_dict["parameters"], dict): + _dict["parameters"] = Parameters.from_dict(_dict["parameters"]) + if "headers" in _dict and isinstance(_dict["headers"], list): + _dict["headers"] = [Header.from_dict(header) for header in _dict["headers"]] + if "endpoint" in _dict and isinstance(_dict["endpoint"], dict): + _dict["endpoint"] = Endpoint.from_dict(_dict["endpoint"]) + return _dict[key] + + +@dataclass +class CartesiaVoice(BaseResponse): + """ + This class defines the voice for the Cartesia model. + """ + + mode: str = field( + default="", metadata=dataclass_config(exclude=lambda f: f is None or f == "") + ) + id: str = field( + default="", metadata=dataclass_config(exclude=lambda f: f is None or f == "") + ) + + +@dataclass +class ListenProvider(BaseResponse): + """ + This class defines the provider for the Listen model. + """ + + type: str = field(default="") + model: str = field(default="") + keyterms: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "keyterms" in _dict and isinstance(_dict["keyterms"], list): + _dict["keyterms"] = [str(keyterm) for keyterm in _dict["keyterms"]] + return _dict[key] + + +@dataclass +class ThinkProvider(BaseResponse): + """ + This class defines the provider for the Think model. + """ + + type: Optional[str] = field(default=None) + model: Optional[str] = field(default=None) + temperature: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + +@dataclass +class SpeakProvider(BaseResponse): + """ + This class defines the provider for the Speak model. + """ + + type: Optional[str] = field(default="deepgram") + """ + Deepgram OR OpenAI model to use. + """ + model: Optional[str] = field( + default="aura-2-thalia-en", + metadata=dataclass_config(exclude=lambda f: f is None), + ) + """ + ElevenLabs or Cartesia model to use. + """ + model_id: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + """ + Cartesia voice configuration. + """ + voice: Optional[CartesiaVoice] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + """ + Cartesia language. + """ + language: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + """ + ElevenLabs language. + """ + language_code: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "voice" in _dict and isinstance(_dict["voice"], dict): + _dict["voice"] = CartesiaVoice.from_dict(_dict["voice"]) + return _dict[key] + + +@dataclass +class Think(BaseResponse): + """ + This class defines any configuration settings for the Think model. + """ + + provider: ThinkProvider = field(default_factory=ThinkProvider) + functions: Optional[List[Function]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + endpoint: Optional[Endpoint] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + prompt: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "provider" in _dict and isinstance(_dict["provider"], dict): + _dict["provider"] = ThinkProvider.from_dict(_dict["provider"]) + if "functions" in _dict and isinstance(_dict["functions"], list): + _dict["functions"] = [ + Function.from_dict(function) for function in _dict["functions"] + ] + if "endpoint" in _dict and isinstance(_dict["endpoint"], dict): + _dict["endpoint"] = Endpoint.from_dict(_dict["endpoint"]) + return _dict[key] + + +@dataclass +class Listen(BaseResponse): + """ + This class defines any configuration settings for the Listen model. + """ + + provider: ListenProvider = field(default_factory=ListenProvider) + + def __getitem__(self, key): + _dict = self.to_dict() + if "provider" in _dict and isinstance(_dict["provider"], dict): + _dict["provider"] = ListenProvider.from_dict(_dict["provider"]) + return _dict[key] + + +@dataclass +class Speak(BaseResponse): + """ + This class defines any configuration settings for the Speak model. + """ + + provider: SpeakProvider = field(default_factory=SpeakProvider) + endpoint: Optional[Endpoint] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "provider" in _dict and isinstance(_dict["provider"], dict): + _dict["provider"] = SpeakProvider.from_dict(_dict["provider"]) + if "endpoint" in _dict and isinstance(_dict["endpoint"], dict): + _dict["endpoint"] = Endpoint.from_dict(_dict["endpoint"]) + return _dict[key] + + +@dataclass +class Agent(BaseResponse): + """ + This class defines any configuration settings for the Agent model. + """ + + listen: Listen = field(default_factory=Listen) + think: Think = field(default_factory=Think) + speak: Speak = field(default_factory=Speak) + greeting: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "listen" in _dict and isinstance(_dict["listen"], dict): + _dict["listen"] = Listen.from_dict(_dict["listen"]) + if "think" in _dict and isinstance(_dict["think"], dict): + _dict["think"] = Think.from_dict(_dict["think"]) + if "speak" in _dict and isinstance(_dict["speak"], dict): + _dict["speak"] = Speak.from_dict(_dict["speak"]) + return _dict[key] + + +@dataclass +class Input(BaseResponse): + """ + This class defines any configuration settings for the input audio. + """ + + encoding: Optional[str] = field(default="linear16") + sample_rate: int = field(default=16000) + + +@dataclass +class Output(BaseResponse): + """ + This class defines any configuration settings for the output audio. + """ + + encoding: Optional[str] = field(default="linear16") + sample_rate: Optional[int] = field(default=16000) + bitrate: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + container: Optional[str] = field(default="none") + + +@dataclass +class Audio(BaseResponse): + """ + This class defines any configuration settings for the audio. + """ + + input: Optional[Input] = field(default_factory=Input) + output: Optional[Output] = field(default_factory=Output) + + def __getitem__(self, key): + _dict = self.to_dict() + if "input" in _dict and isinstance(_dict["input"], dict): + _dict["input"] = Input.from_dict(_dict["input"]) + if "output" in _dict and isinstance(_dict["output"], dict): + _dict["output"] = Output.from_dict(_dict["output"]) + return _dict[key] + + +@dataclass +class Language(BaseResponse): + """ + Define the language for the agent. + """ + + type: str = field(default="en") + + +@dataclass +class SettingsOptions(BaseResponse): + """ + The client should send a Settings message immediately after opening the websocket and before sending any audio. + """ + + experimental: Optional[bool] = field(default=False) + type: str = str(AgentWebSocketEvents.Settings) + audio: Audio = field(default_factory=Audio) + agent: Agent = field(default_factory=Agent) + + def __getitem__(self, key): + _dict = self.to_dict() + if "audio" in _dict and isinstance(_dict["audio"], dict): + _dict["audio"] = Audio.from_dict(_dict["audio"]) + if "agent" in _dict and isinstance(_dict["agent"], dict): + _dict["agent"] = Agent.from_dict(_dict["agent"]) + return _dict[key] + + def check(self): + """ + Check the options for any deprecated or soon-to-be-deprecated options. + """ + logger = verboselogs.VerboseLogger(__name__) + logger.addHandler(logging.StreamHandler()) + prev = logger.level + logger.setLevel(verboselogs.ERROR) + + # do we need to check anything here? + + logger.setLevel(prev) + + return True + + +# UpdatePrompt + + +@dataclass +class UpdatePromptOptions(BaseResponse): + """ + The client can send an UpdatePrompt message to provide a new prompt to the Think model in the middle of a conversation. + """ + + type: str = str(AgentWebSocketEvents.UpdatePrompt) + prompt: str = field(default="") + + +# UpdateSpeak + + +@dataclass +class UpdateSpeakOptions(BaseResponse): + """ + The client can send an UpdateSpeak message to change the Speak model in the middle of a conversation. + """ + + type: str = str(AgentWebSocketEvents.UpdateSpeak) + speak: Speak = field(default_factory=Speak) + + +# InjectAgentMessage + + +@dataclass +class InjectAgentMessageOptions(BaseResponse): + """ + The client can send an InjectAgentMessage to immediately trigger an agent statement. If the injection request arrives while the user is speaking, or while the server is in the middle of sending audio for an agent response, then the request will be ignored and the server will reply with an InjectionRefused. + """ + + type: str = str(AgentWebSocketEvents.InjectAgentMessage) + message: str = field(default="") + + +# Function Call Response + + +@dataclass +class FunctionCallResponse(BaseResponse): + """ + TheFunctionCallResponse message is a JSON command that the client should reply with every time there is a FunctionCallRequest received. + """ + + type: str = "FunctionCallResponse" + function_call_id: str = field(default="") + output: str = field(default="") + + +# Agent Keep Alive + + +@dataclass +class AgentKeepAlive(BaseResponse): + """ + The KeepAlive message is a JSON command that you can use to ensure that the server does not close the connection. + """ + + type: str = "KeepAlive" diff --git a/deepgram/clients/agent/v1/websocket/response.py b/deepgram/clients/agent/v1/websocket/response.py new file mode 100644 index 0000000000000000000000000000000000000000..c0dc226d84fa31e4271fa1519b88e17a462cdac2 --- /dev/null +++ b/deepgram/clients/agent/v1/websocket/response.py @@ -0,0 +1,109 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Dict, Any + +from dataclasses import dataclass + +# common websocket response +from ....common import ( + BaseResponse, + OpenResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) + +# unique + + +@dataclass +class WelcomeResponse(BaseResponse): + """ + The server will send a Welcome message as soon as the websocket opens. + """ + + type: str + request_id: str + + +@dataclass +class SettingsAppliedResponse(BaseResponse): + """ + The server will send a SettingsApplied message as soon as the settings are applied. + """ + + type: str + + +@dataclass +class ConversationTextResponse(BaseResponse): + """ + The server will send a ConversationText message every time the agent hears the user say something, and every time the agent speaks something itself. + """ + + type: str + role: str + content: str + + +@dataclass +class UserStartedSpeakingResponse(BaseResponse): + """ + The server will send a UserStartedSpeaking message every time the user begins a new utterance. + """ + + type: str + + +@dataclass +class AgentThinkingResponse(BaseResponse): + """ + The server will send an AgentThinking message to inform the client of a non-verbalized agent thought. + You will ONLY receive this message if you have set `experimental` to true. + """ + + type: str + content: str + + +@dataclass +class FunctionCallRequest(BaseResponse): + """ + The FunctionCallRequest message is used to call a function from the server to the client. + """ + + type: str + function_name: str + function_call_id: str + input: str + + +@dataclass +class AgentStartedSpeakingResponse(BaseResponse): + """ + The server will send an AgentStartedSpeaking message when it begins streaming an agent audio response to the client for playback. + """ + + total_latency: float + tts_latency: float + ttt_latency: float + + +@dataclass +class AgentAudioDoneResponse(BaseResponse): + """ + The server will send an AgentAudioDone message immediately after it sends the last audio message in a piece of agent speech. + """ + + type: str + + +@dataclass +class InjectionRefusedResponse(BaseResponse): + """ + The server will send an InjectionRefused message when an InjectAgentMessage request is ignored because it arrived while the user was speaking or while the server was sending audio for an agent response. + """ + + type: str diff --git a/deepgram/clients/agent_router.py b/deepgram/clients/agent_router.py new file mode 100644 index 0000000000000000000000000000000000000000..90d82f3a5410f32b0ed85aa3c05a6f516756e3e3 --- /dev/null +++ b/deepgram/clients/agent_router.py @@ -0,0 +1,130 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from importlib import import_module +import logging + +from ..utils import verboselogs +from ..options import DeepgramClientOptions +from .errors import DeepgramModuleError + + +class AgentRouter: + """ + Represents a client for interacting with the Deepgram API. + + This class provides a client for making requests to the Deepgram API with various configuration options. + + Attributes: + config_options (DeepgramClientOptions): An optional configuration object specifying client options. + + Raises: + DeepgramApiKeyError: If the API key is missing or invalid. + + Methods: + read: (Preferred) Returns an Threaded AnalyzeClient instance for interacting with Deepgram's read transcription services. + asyncread: Returns an (Async) AnalyzeClient instance for interacting with Deepgram's read transcription services. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + + @property + def websocket(self): + """ + Returns an AgentWebSocketClient instance for interacting with Deepgram's Agent API. + """ + return self.Version(self._config, "websocket") + + @property + def asyncwebsocket(self): + """ + Returns an AsyncAgentWebSocketClient instance for interacting with Deepgram's Agent API. + """ + return self.Version(self._config, "asyncwebsocket") + + # INTERNAL CLASSES + class Version: + """ + Represents a version of the Deepgram API. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _parent: str + + def __init__(self, config, parent: str): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._parent = parent + + # FUTURE VERSIONING: + # When v2 or v1.1beta1 or etc. This allows easy access to the latest version of the API. + # @property + # def latest(self): + # match self._parent: + # case "analyze": + # return AnalyzeClient(self._config) + # case _: + # raise DeepgramModuleError("Invalid parent") + + def v(self, version: str = ""): + """ + Returns a specific version of the Deepgram API. + """ + self._logger.debug("Version.v ENTER") + self._logger.info("version: %s", version) + if len(version) == 0: + self._logger.error("version is empty") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid module version") + + parent = "" + file_name = "" + class_name = "" + match self._parent: + case "websocket": + parent = "websocket" + file_name = "client" + class_name = "AgentWebSocketClient" + case "asyncwebsocket": + parent = "websocket" + file_name = "async_client" + class_name = "AsyncAgentWebSocketClient" + case _: + self._logger.error("parent unknown: %s", self._parent) + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid parent type") + + # create class path + path = f"deepgram.clients.agent.v{version}.{parent}.{file_name}" + self._logger.info("path: %s", path) + self._logger.info("class_name: %s", class_name) + + # import class + mod = import_module(path) + if mod is None: + self._logger.error("module path is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find package") + + my_class = getattr(mod, class_name) + if my_class is None: + self._logger.error("my_class is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find class") + + # instantiate class + my_class = my_class(self._config) + self._logger.notice("Version.v succeeded") + self._logger.debug("Version.v LEAVE") + return my_class diff --git a/deepgram/clients/analyze/__init__.py b/deepgram/clients/analyze/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..71708c58116ae0667986b1dd660c4d45b2424d06 --- /dev/null +++ b/deepgram/clients/analyze/__init__.py @@ -0,0 +1,40 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import AnalyzeClient, AsyncAnalyzeClient +from .client import ReadClient, AsyncReadClient +from .client import AnalyzeOptions +from .client import ( + # common + UrlSource, + TextSource, + BufferSource, + StreamSource, + FileSource, + # unique + AnalyzeStreamSource, + AnalyzeSource, +) +from .client import ( + AsyncAnalyzeResponse, + SyncAnalyzeResponse, + AnalyzeResponse, + # shared + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, + # unique + AnalyzeMetadata, + AnalyzeResults, + AnalyzeSummary, +) diff --git a/deepgram/clients/analyze/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/analyze/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbb7123e1c63bad498f4eb678d6395eee463da02 Binary files /dev/null and b/deepgram/clients/analyze/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/analyze/__pycache__/client.cpython-313.pyc b/deepgram/clients/analyze/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac15dd7234dbcf35a0e4ee8fc4721ac96a334946 Binary files /dev/null and b/deepgram/clients/analyze/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/analyze/client.py b/deepgram/clients/analyze/client.py new file mode 100644 index 0000000000000000000000000000000000000000..80dc3fad55669889296498bad629e5304dcebbb9 --- /dev/null +++ b/deepgram/clients/analyze/client.py @@ -0,0 +1,87 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .v1.client import AnalyzeClient as AnalyzeClientLatest +from .v1.async_client import AsyncAnalyzeClient as AsyncAnalyzeClientLatest +from .v1.options import ( + # common + AnalyzeOptions as AnalyzeOptionsLatest, + UrlSource as UrlSourceLatest, + TextSource as TextSourceLatest, + BufferSource as BufferSourceLatest, + StreamSource as StreamSourceLatest, + FileSource as FileSourceLatest, + # unique + AnalyzeStreamSource as AnalyzeStreamSourceLatest, + AnalyzeSource as AnalyzeSourceLatest, +) +from .v1.response import ( + SyncAnalyzeResponse as SyncAnalyzeResponseLatest, + AnalyzeResponse as AnalyzeResponseLatest, + AsyncAnalyzeResponse as AsyncAnalyzeResponseLatest, + # shared + Average as AverageLatest, + Intent as IntentLatest, + Intents as IntentsLatest, + IntentsInfo as IntentsInfoLatest, + Segment as SegmentLatest, + SentimentInfo as SentimentInfoLatest, + Sentiment as SentimentLatest, + Sentiments as SentimentsLatest, + SummaryInfo as SummaryInfoLatest, + Topic as TopicLatest, + Topics as TopicsLatest, + TopicsInfo as TopicsInfoLatest, + # unique + Results as ResultsLatest, + Metadata as MetadataLatest, + Summary as SummaryLatest, +) + +# The client.py points to the current supported version in the SDK. +# Older versions are supported in the SDK for backwards compatibility. + +# common +UrlSource = UrlSourceLatest +TextSource = TextSourceLatest +BufferSource = BufferSourceLatest +StreamSource = StreamSourceLatest +FileSource = FileSourceLatest + +AnalyzeStreamSource = AnalyzeStreamSourceLatest +AnalyzeSource = AnalyzeSourceLatest + +# input +AnalyzeOptions = AnalyzeOptionsLatest + +# responses +SyncAnalyzeResponse = SyncAnalyzeResponseLatest +AnalyzeResponse = AnalyzeResponseLatest +AsyncAnalyzeResponse = AsyncAnalyzeResponseLatest +# shared +Average = AverageLatest +Intent = IntentLatest +Intents = IntentsLatest +IntentsInfo = IntentsInfoLatest +Segment = SegmentLatest +SentimentInfo = SentimentInfoLatest +Sentiment = SentimentLatest +Sentiments = SentimentsLatest +SummaryInfo = SummaryInfoLatest +Topic = TopicLatest +Topics = TopicsLatest +TopicsInfo = TopicsInfoLatest +# unique +AnalyzeResults = ResultsLatest +AnalyzeMetadata = MetadataLatest +AnalyzeSummary = SummaryLatest + +# clients +AnalyzeClient = AnalyzeClientLatest +AsyncAnalyzeClient = AsyncAnalyzeClientLatest + + +# aliases +ReadClient = AnalyzeClientLatest +AsyncReadClient = AsyncAnalyzeClientLatest diff --git a/deepgram/clients/analyze/v1/__init__.py b/deepgram/clients/analyze/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8ffd0b463e5b4630c670b5a6c40bbcbe52ce7d23 --- /dev/null +++ b/deepgram/clients/analyze/v1/__init__.py @@ -0,0 +1,46 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import AnalyzeClient +from .async_client import AsyncAnalyzeClient + +# common +from .options import ( + UrlSource, + TextSource, + BufferSource, + StreamSource, + FileSource, +) + +# analyze + +from .options import ( + AnalyzeOptions, + AnalyzeStreamSource, + AnalyzeSource, +) + +from .response import ( + AsyncAnalyzeResponse, + SyncAnalyzeResponse, + AnalyzeResponse, + # shared + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, + # unique + Metadata, + Results, + Summary, +) diff --git a/deepgram/clients/analyze/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/analyze/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a3de861c820936e99b6229ec291ae1a7b7da754 Binary files /dev/null and b/deepgram/clients/analyze/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/analyze/v1/__pycache__/async_client.cpython-313.pyc b/deepgram/clients/analyze/v1/__pycache__/async_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..537e1fb9fa2b5c48309693596b6089586b5a27de Binary files /dev/null and b/deepgram/clients/analyze/v1/__pycache__/async_client.cpython-313.pyc differ diff --git a/deepgram/clients/analyze/v1/__pycache__/client.cpython-313.pyc b/deepgram/clients/analyze/v1/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfd2d62b3569d278d9dd7a77c0fc275eff469db6 Binary files /dev/null and b/deepgram/clients/analyze/v1/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/analyze/v1/__pycache__/helpers.cpython-313.pyc b/deepgram/clients/analyze/v1/__pycache__/helpers.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a77e68a8586bdb8b225582e72b6fb294c676dcb5 Binary files /dev/null and b/deepgram/clients/analyze/v1/__pycache__/helpers.cpython-313.pyc differ diff --git a/deepgram/clients/analyze/v1/__pycache__/options.cpython-313.pyc b/deepgram/clients/analyze/v1/__pycache__/options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a609f79f3e6b859d8b5db99e628b5a9431473f1a Binary files /dev/null and b/deepgram/clients/analyze/v1/__pycache__/options.cpython-313.pyc differ diff --git a/deepgram/clients/analyze/v1/__pycache__/response.cpython-313.pyc b/deepgram/clients/analyze/v1/__pycache__/response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..574a3d735ae3851b6f2e311e4bbd33db519e1997 Binary files /dev/null and b/deepgram/clients/analyze/v1/__pycache__/response.cpython-313.pyc differ diff --git a/deepgram/clients/analyze/v1/async_client.py b/deepgram/clients/analyze/v1/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..5c3e569bf2b76c21b93b5e68abb0e9761c0a8790 --- /dev/null +++ b/deepgram/clients/analyze/v1/async_client.py @@ -0,0 +1,346 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Dict, Union, Optional + +import httpx + +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from ...common import AbstractAsyncRestClient +from ...common import DeepgramError, DeepgramTypeError + +from .helpers import is_buffer_source, is_readstream_source, is_url_source +from .options import ( + AnalyzeOptions, + UrlSource, + FileSource, +) +from .response import AsyncAnalyzeResponse, AnalyzeResponse + + +class AsyncAnalyzeClient(AbstractAsyncRestClient): + """ + A client class for handling text data. + Provides methods for transcribing text from URLs and files. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + super().__init__(config) + + # pylint: disable=too-many-positional-arguments + + async def analyze_url( + self, + source: UrlSource, + options: Optional[Union[AnalyzeOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/read", + **kwargs, + ) -> Union[AsyncAnalyzeResponse, AnalyzeResponse]: + """ + Analyze text from a URL source. + + Args: + source (UrlSource): The URL source of the text to ingest. + options (AnalyzeOptions): Additional options for the ingest (default is None). + endpoint (str): The API endpoint for the ingest (default is "v1/read"). + + Returns: + AnalyzeResponse: An object containing the result. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AsyncAnalyzeClient.analyze_url ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, AnalyzeOptions) and options.callback is not None): + self._logger.debug("AsyncAnalyzeClient.analyze_url LEAVE") + return await self.analyze_url_callback( + source, + callback=options["callback"], + options=options, + headers=headers, + addons=addons, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + url = f"{self._config.url}/{endpoint}" + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("AsyncAnalyzeClient.analyze_url LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, AnalyzeOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AsyncAnalyzeClient.analyze_url LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, AnalyzeOptions): + self._logger.info("AnalyzeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AnalyzeResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("analyze_url succeeded") + self._logger.debug("AsyncAnalyzeClient.analyze_url LEAVE") + return res + + async def analyze_url_callback( + self, + source: UrlSource, + callback: str, + options: Optional[Union[AnalyzeOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/read", + **kwargs, + ) -> AsyncAnalyzeResponse: + """ + Transcribes audio from a URL source and sends the result to a callback URL. + + Args: + source (UrlSource): The URL source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (AnalyzeOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/read"). + + Returns: + AsyncAnalyzeResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AnalyzeClient.analyze_url_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, AnalyzeOptions): + options.callback = callback + else: + options["callback"] = callback + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("AnalyzeClient.analyze_url_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, AnalyzeOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AnalyzeClient.analyze_url_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, AnalyzeOptions): + self._logger.info("AnalyzeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AsyncAnalyzeResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("analyze_url_callback succeeded") + self._logger.debug("AnalyzeClient.analyze_url_callback LEAVE") + return res + + async def analyze_text( + self, + source: FileSource, + options: Optional[Union[AnalyzeOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/read", + **kwargs, + ) -> Union[AsyncAnalyzeResponse, AnalyzeResponse]: + """ + Analyze text from a local file source. + + Args: + source (TextSource): The local file source of the text to ingest. + options (AnalyzeOptions): Additional options for the ingest (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/read"). + + Returns: + AnalyzeResponse: An object containing the transcription result or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AsyncAnalyzeClient.analyze_text ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, AnalyzeOptions) and options.callback is not None): + self._logger.debug("AsyncAnalyzeClient.analyze_text LEAVE") + return await self.analyze_text_callback( + source, + callback=options["callback"], + options=options, + headers=headers, + addons=addons, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + url = f"{self._config.url}/{endpoint}" + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("AsyncAnalyzeClient.analyze_text LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, AnalyzeOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AsyncAnalyzeClient.analyze_text LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, AnalyzeOptions): + self._logger.info("AnalyzeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, + options=options, + addons=addons, + headers=headers, + content=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AnalyzeResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("analyze_text succeeded") + self._logger.debug("AsyncAnalyzeClient.analyze_text LEAVE") + return res + + async def analyze_text_callback( + self, + source: FileSource, + callback: str, + options: Optional[Union[AnalyzeOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/read", + **kwargs, + ) -> AsyncAnalyzeResponse: + """ + Transcribes audio from a local file source and sends the result to a callback URL. + + Args: + source (TextSource): The local file source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (AnalyzeOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/read"). + + Returns: + AsyncAnalyzeResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AnalyzeClient.analyze_text_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, AnalyzeOptions): + options.callback = callback + else: + options["callback"] = callback + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("AnalyzeClient.analyze_text_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, AnalyzeOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AnalyzeClient.analyze_text_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, AnalyzeOptions): + self._logger.info("AnalyzeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AsyncAnalyzeResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("analyze_text_callback succeeded") + self._logger.debug("AnalyzeClient.analyze_text_callback LEAVE") + return res + + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/analyze/v1/client.py b/deepgram/clients/analyze/v1/client.py new file mode 100644 index 0000000000000000000000000000000000000000..a90145ede0bd773cd58129978a85cd48d765a2de --- /dev/null +++ b/deepgram/clients/analyze/v1/client.py @@ -0,0 +1,346 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Dict, Union, Optional + +import httpx + +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from ...common import AbstractSyncRestClient +from ...common import DeepgramError, DeepgramTypeError + +from .helpers import is_buffer_source, is_readstream_source, is_url_source +from .options import ( + AnalyzeOptions, + UrlSource, + FileSource, +) +from .response import AsyncAnalyzeResponse, AnalyzeResponse + + +class AnalyzeClient(AbstractSyncRestClient): + """ + A client class for handling text data. + Provides methods for transcribing text from URLs, files, etc. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + super().__init__(config) + + # pylint: disable=too-many-positional-arguments + + def analyze_url( + self, + source: UrlSource, + options: Optional[Union[AnalyzeOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/read", + **kwargs, + ) -> Union[AnalyzeResponse, AsyncAnalyzeResponse]: + """ + Analyze text from a URL source. + + Args: + source (UrlSource): The URL source of the text to ingest. + options (AnalyzeOptions): Additional options for the ingest (default is None). + endpoint (str): The API endpoint for the ingest (default is "v1/read"). + + Returns: + AnalyzeResponse: An object containing the result. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AnalyzeClient.analyze_url ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, AnalyzeOptions) and options.callback is not None): + self._logger.debug("AnalyzeClient.analyze_url LEAVE") + return self.analyze_url_callback( + source, + callback=options["callback"], + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + url = f"{self._config.url}/{endpoint}" + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("AnalyzeClient.analyze_url LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, AnalyzeOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AnalyzeClient.analyze_url LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, AnalyzeOptions): + self._logger.info("AnalyzeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AnalyzeResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("analyze_url succeeded") + self._logger.debug("AnalyzeClient.analyze_url LEAVE") + return res + + def analyze_url_callback( + self, + source: UrlSource, + callback: str, + options: Optional[Union[AnalyzeOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/read", + **kwargs, + ) -> AsyncAnalyzeResponse: + """ + Transcribes audio from a URL source and sends the result to a callback URL. + + Args: + source (UrlSource): The URL source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (AnalyzeOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/read"). + + Returns: + AsyncAnalyzeResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AnalyzeClient.analyze_url_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, AnalyzeOptions): + options.callback = callback + else: + options["callback"] = callback + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("AnalyzeClient.analyze_url_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, AnalyzeOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AnalyzeClient.analyze_url_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, AnalyzeOptions): + self._logger.info("AnalyzeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AsyncAnalyzeResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("analyze_url_callback succeeded") + self._logger.debug("AnalyzeClient.analyze_url_callback LEAVE") + return res + + def analyze_text( + self, + source: FileSource, + options: Optional[Union[AnalyzeOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/read", + **kwargs, + ) -> Union[AnalyzeResponse, AsyncAnalyzeResponse]: + """ + Analyze text from a local file source. + + Args: + source (TextSource): The local file source of the text to ingest. + options (AnalyzeOptions): Additional options for the ingest (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/read"). + + Returns: + AnalyzeResponse: An object containing the transcription result or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AnalyzeClient.analyze_text ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, AnalyzeOptions) and options.callback is not None): + self._logger.debug("AnalyzeClient.analyze_text LEAVE") + return self.analyze_text_callback( + source, + callback=options["callback"], + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + url = f"{self._config.url}/{endpoint}" + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("AnalyzeClient.analyze_text LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, AnalyzeOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AnalyzeClient.analyze_text LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, AnalyzeOptions): + self._logger.info("AnalyzeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + content=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AnalyzeResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("analyze_text succeeded") + self._logger.debug("AnalyzeClient.analyze_text LEAVE") + return res + + def analyze_text_callback( + self, + source: FileSource, + callback: str, + options: Optional[Union[AnalyzeOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/read", + **kwargs, + ) -> AsyncAnalyzeResponse: + """ + Transcribes audio from a local file source and sends the result to a callback URL. + + Args: + source (TextSource): The local file source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (AnalyzeOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/read"). + + Returns: + AsyncAnalyzeResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AnalyzeClient.analyze_file_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, AnalyzeOptions): + options.callback = callback + else: + options["callback"] = callback + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("AnalyzeClient.analyze_file_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, AnalyzeOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AnalyzeClient.analyze_file_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, AnalyzeOptions): + self._logger.info("AnalyzeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AsyncAnalyzeResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("analyze_file_callback succeeded") + self._logger.debug("AnalyzeClient.analyze_file_callback LEAVE") + return res + + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/analyze/v1/helpers.py b/deepgram/clients/analyze/v1/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..ef6746b33a6c95d0a87c46f195a65d4460555199 --- /dev/null +++ b/deepgram/clients/analyze/v1/helpers.py @@ -0,0 +1,26 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .options import AnalyzeSource + + +def is_buffer_source(provided_source: AnalyzeSource) -> bool: + """ + Check if the provided source is a buffer source. + """ + return "buffer" in provided_source + + +def is_readstream_source(provided_source: AnalyzeSource) -> bool: + """ + Check if the provided source is a readstream source. + """ + return "stream" in provided_source + + +def is_url_source(provided_source: AnalyzeSource) -> bool: + """ + Check if the provided source is a url source. + """ + return "url" in provided_source diff --git a/deepgram/clients/analyze/v1/options.py b/deepgram/clients/analyze/v1/options.py new file mode 100644 index 0000000000000000000000000000000000000000..d6999c6675ecaebb8c7c107d4c8c8bf18de0657c --- /dev/null +++ b/deepgram/clients/analyze/v1/options.py @@ -0,0 +1,83 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import List, Union, Optional + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from ....utils import verboselogs +from ...common import ( + TextSource, + FileSource, + BufferSource, + StreamSource, + UrlSource, + BaseResponse, +) + + +@dataclass +class AnalyzeOptions(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + Contains all the options for the AnalyzeOptions. + + Reference: + https://developers.deepgram.com/reference/text-intelligence-apis + """ + + callback: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + callback_method: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + custom_intent: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + custom_intent_mode: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + custom_topic: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + custom_topic_mode: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + intents: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + language: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + summarize: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + topics: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def check(self): + """ + Check the options for the AnalyzeOptions. + """ + logger = verboselogs.VerboseLogger(__name__) + logger.addHandler(logging.StreamHandler()) + prev = logger.level + logger.setLevel(verboselogs.ERROR) + + # no op at the moment + + logger.setLevel(prev) + + return True + + +# unique +AnalyzeSource = Union[UrlSource, FileSource] +AnalyzeStreamSource = StreamSource diff --git a/deepgram/clients/analyze/v1/response.py b/deepgram/clients/analyze/v1/response.py new file mode 100644 index 0000000000000000000000000000000000000000..5d678cba378e960b5f874a8539abe88fc8c01b14 --- /dev/null +++ b/deepgram/clients/analyze/v1/response.py @@ -0,0 +1,143 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Dict, Any + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from ...common import ( + BaseResponse, + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, +) + + +# Async Analyze Response Types: + + +@dataclass +class AsyncAnalyzeResponse(BaseResponse): + """ + Async Analyze Response + """ + + request_id: str = "" + + +# Analyze Response Types: + + +@dataclass +class Metadata(BaseResponse): + """ + Metadata + """ + + request_id: str = "" + created: str = "" + language: str = "" + intents_info: Optional[IntentsInfo] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment_info: Optional[SentimentInfo] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + summary_info: Optional[SummaryInfo] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + topics_info: Optional[TopicsInfo] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "intents_info" in _dict: + _dict["intents_info"] = IntentsInfo.from_dict(_dict["intents_info"]) + if "sentiment_info" in _dict: + _dict["sentiment_info"] = SentimentInfo.from_dict(_dict["sentiment_info"]) + if "summary_info" in _dict: + _dict["summary_info"] = SummaryInfo.from_dict(_dict["summary_info"]) + if "topics_info" in _dict: + _dict["topics_info"] = TopicsInfo.from_dict(_dict["topics_info"]) + return _dict[key] + + +@dataclass +class Summary(BaseResponse): + """ + Summary + """ + + text: str = "" + + +@dataclass +class Results(BaseResponse): + """ + Results + """ + + summary: Optional[Summary] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiments: Optional[Sentiments] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + topics: Optional[Topics] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + intents: Optional[Intents] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "summary" in _dict: + _dict["summary"] = Summary.from_dict(_dict["summary"]) + if "sentiments" in _dict: + _dict["sentiments"] = Sentiments.from_dict(_dict["sentiments"]) + if "topics" in _dict: + _dict["topics"] = Topics.from_dict(_dict["topics"]) + if "intents" in _dict: + _dict["intents"] = Intents.from_dict(_dict["intents"]) + return _dict[key] + + +# Analyze Response Result: + + +@dataclass +class AnalyzeResponse(BaseResponse): + """ + Analyze Response + """ + + metadata: Optional[Metadata] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + results: Optional[Results] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "metadata" in _dict: + _dict["metadata"] = Metadata.from_dict(_dict["metadata"]) + if "results" in _dict: + _dict["results"] = Results.from_dict(_dict["results"]) + return _dict[key] + + +SyncAnalyzeResponse = AnalyzeResponse diff --git a/deepgram/clients/auth/__init__.py b/deepgram/clients/auth/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d5ac419cfbe977c7a172afb76dce634af6405f3a --- /dev/null +++ b/deepgram/clients/auth/__init__.py @@ -0,0 +1,9 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import AuthRESTClient +from .client import AsyncAuthRESTClient +from .client import ( + GrantTokenResponse, +) \ No newline at end of file diff --git a/deepgram/clients/auth/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/auth/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0e1d357e63365156ba9933b2831dd01c7ef7168 Binary files /dev/null and b/deepgram/clients/auth/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/auth/__pycache__/client.cpython-313.pyc b/deepgram/clients/auth/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5bdb91417c0c2bb5ca7c6abb94aed72cf9cea429 Binary files /dev/null and b/deepgram/clients/auth/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/auth/client.py b/deepgram/clients/auth/client.py new file mode 100644 index 0000000000000000000000000000000000000000..2e74f243409c7e2a27f32addb98c3f80a71435fa --- /dev/null +++ b/deepgram/clients/auth/client.py @@ -0,0 +1,11 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .v1.client import AuthRESTClient as AuthRESTClientLatest +from .v1.async_client import AsyncAuthRESTClient as AsyncAuthRESTClientLatest +from .v1.response import GrantTokenResponse as GrantTokenResponseLatest + +AuthRESTClient = AuthRESTClientLatest +AsyncAuthRESTClient = AsyncAuthRESTClientLatest +GrantTokenResponse = GrantTokenResponseLatest diff --git a/deepgram/clients/auth/v1/__init__.py b/deepgram/clients/auth/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f449c80adca6af8c0f695b97d02e423455674cea --- /dev/null +++ b/deepgram/clients/auth/v1/__init__.py @@ -0,0 +1,8 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT +from .client import AuthRESTClient +from .async_client import AsyncAuthRESTClient +from .response import ( + GrantTokenResponse, +) \ No newline at end of file diff --git a/deepgram/clients/auth/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/auth/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..763a6ab88252351bc6375fe055482e7ed587916a Binary files /dev/null and b/deepgram/clients/auth/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/auth/v1/__pycache__/async_client.cpython-313.pyc b/deepgram/clients/auth/v1/__pycache__/async_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d45b67cea8ad5a789cd0be7eaaa115019d5c779 Binary files /dev/null and b/deepgram/clients/auth/v1/__pycache__/async_client.cpython-313.pyc differ diff --git a/deepgram/clients/auth/v1/__pycache__/client.cpython-313.pyc b/deepgram/clients/auth/v1/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f2ac0b4292908f898247aaff1ef713652b9af0f Binary files /dev/null and b/deepgram/clients/auth/v1/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/auth/v1/__pycache__/response.cpython-313.pyc b/deepgram/clients/auth/v1/__pycache__/response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ce98d68ea7fb7754f61d6efcc2d98ed26270bdd Binary files /dev/null and b/deepgram/clients/auth/v1/__pycache__/response.cpython-313.pyc differ diff --git a/deepgram/clients/auth/v1/async_client.py b/deepgram/clients/auth/v1/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..43ecb49067c00cebdcfb78f125c7b3e378cf61c1 --- /dev/null +++ b/deepgram/clients/auth/v1/async_client.py @@ -0,0 +1,51 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging + +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from ...common import AbstractAsyncRestClient +from .response import GrantTokenResponse + + +class AsyncAuthRESTClient(AbstractAsyncRestClient): + """ + A client class for handling authentication endpoints. + Provides method for generating a temporary JWT token. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._endpoint = "v1/auth/grant" + super().__init__(config) + + async def grant_token(self): + """ + Generates a temporary JWT with a 30 second TTL. + + Returns: + GrantTokenResponse: An object containing the authentication token and its expiration time. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AuthRestClient.grant_token ENTER") + + url = f"{self._config.url}/{self._endpoint}" + self._logger.info("url: %s", url) + result = await self.post(url, headers={"Authorization": f"Token {self._config.api_key}"}) + self._logger.info("json: %s", result) + res = GrantTokenResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("grant_token succeeded") + self._logger.debug("AuthRestClient.grant_token LEAVE") + return res diff --git a/deepgram/clients/auth/v1/client.py b/deepgram/clients/auth/v1/client.py new file mode 100644 index 0000000000000000000000000000000000000000..c75815daa50729a2dae2218e88f91c93b4d5e896 --- /dev/null +++ b/deepgram/clients/auth/v1/client.py @@ -0,0 +1,51 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging + +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from ...common import AbstractSyncRestClient +from .response import GrantTokenResponse + + +class AuthRESTClient(AbstractSyncRestClient): + """ + A client class for handling authentication endpoints. + Provides method for generating a temporary JWT token. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._endpoint = "v1/auth/grant" + super().__init__(config) + + def grant_token(self): + """ + Generates a temporary JWT with a 30 second TTL. + + Returns: + GrantTokenResponse: An object containing the authentication token and its expiration time. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AuthRestClient.grant_token ENTER") + + url = f"{self._config.url}/{self._endpoint}" + self._logger.info("url: %s", url) + result = self.post(url, headers={"Authorization": f"Token {self._config.api_key}"}) + self._logger.info("json: %s", result) + res = GrantTokenResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("grant_token succeeded") + self._logger.debug("AuthRestClient.grant_token LEAVE") + return res diff --git a/deepgram/clients/auth/v1/response.py b/deepgram/clients/auth/v1/response.py new file mode 100644 index 0000000000000000000000000000000000000000..cf72f3576fcbccce78542ccd0c4fd162c15d6de7 --- /dev/null +++ b/deepgram/clients/auth/v1/response.py @@ -0,0 +1,24 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from ...common import ( + BaseResponse, +) + +@dataclass +class GrantTokenResponse(BaseResponse): + """ + The response object for the authentication grant token endpoint. + """ + access_token: str = field( + metadata=dataclass_config(field_name='access_token'), + default="", + ) + expires_in: int = field( + metadata=dataclass_config(field_name='expires_in'), + default=30, + ) \ No newline at end of file diff --git a/deepgram/clients/common/__init__.py b/deepgram/clients/common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..58c449d9d6aabbfd2776ac5b2e07957dd75cf3c9 --- /dev/null +++ b/deepgram/clients/common/__init__.py @@ -0,0 +1,85 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .v1 import ( + DeepgramError, + DeepgramTypeError, + DeepgramApiError, + DeepgramUnknownApiError, +) + +from .v1 import AbstractAsyncRestClient +from .v1 import AbstractSyncRestClient +from .v1 import AbstractAsyncWebSocketClient +from .v1 import AbstractSyncWebSocketClient + +from .v1 import ( + TextSource as TextSourceLatest, + BufferSource as BufferSourceLatest, + StreamSource as StreamSourceLatest, + FileSource as FileSourceLatest, + UrlSource as UrlSourceLatest, +) + +# shared +from .v1 import ( + BaseResponse as BaseResponseLatest, + ModelInfo as ModelInfoLatest, + Hit as HitLatest, + Search as SearchLatest, +) + +# rest +from .v1 import ( + Average as AverageLatest, + Intent as IntentLatest, + Intents as IntentsLatest, + IntentsInfo as IntentsInfoLatest, + Segment as SegmentLatest, + SentimentInfo as SentimentInfoLatest, + Sentiment as SentimentLatest, + Sentiments as SentimentsLatest, + SummaryInfo as SummaryInfoLatest, + Topic as TopicLatest, + Topics as TopicsLatest, + TopicsInfo as TopicsInfoLatest, +) + +# websocket +from .v1 import ( + OpenResponse as OpenResponseLatest, + CloseResponse as CloseResponseLatest, + ErrorResponse as ErrorResponseLatest, + UnhandledResponse as UnhandledResponseLatest, +) + +# export +UrlSource = UrlSourceLatest +TextSource = TextSourceLatest +BufferSource = BufferSourceLatest +StreamSource = StreamSourceLatest +FileSource = FileSourceLatest + +BaseResponse = BaseResponseLatest +ModelInfo = ModelInfoLatest +Hit = HitLatest +Search = SearchLatest + +Average = AverageLatest +Intent = IntentLatest +Intents = IntentsLatest +IntentsInfo = IntentsInfoLatest +Segment = SegmentLatest +SentimentInfo = SentimentInfoLatest +Sentiment = SentimentLatest +Sentiments = SentimentsLatest +SummaryInfo = SummaryInfoLatest +Topic = TopicLatest +Topics = TopicsLatest +TopicsInfo = TopicsInfoLatest + +OpenResponse = OpenResponseLatest +CloseResponse = CloseResponseLatest +ErrorResponse = ErrorResponseLatest +UnhandledResponse = UnhandledResponseLatest diff --git a/deepgram/clients/common/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/common/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cae1427810c7eaa53643d4054371d79b1fd6438 Binary files /dev/null and b/deepgram/clients/common/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__init__.py b/deepgram/clients/common/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7603fc0a07916d5569801e82454bba85e0d515f6 --- /dev/null +++ b/deepgram/clients/common/v1/__init__.py @@ -0,0 +1,53 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .enums import Sentiment + +from .errors import ( + DeepgramError, + DeepgramTypeError, + DeepgramApiError, + DeepgramUnknownApiError, +) +from .abstract_async_rest import AbstractAsyncRestClient +from .abstract_sync_rest import AbstractSyncRestClient +from .abstract_async_websocket import AbstractAsyncWebSocketClient +from .abstract_sync_websocket import AbstractSyncWebSocketClient + +from .options import ( + TextSource, + BufferSource, + StreamSource, + FileSource, + UrlSource, +) + +from .shared_response import ( + BaseResponse, + ModelInfo, + Hit, + Search, +) + +from .rest_response import ( + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, +) + +from .websocket_response import ( + OpenResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) diff --git a/deepgram/clients/common/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05567481861cf0f6457babbda3e58eef67bf8057 Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/abstract_async_rest.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/abstract_async_rest.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d26568ea6e9ed41dfe1d7613a751de09c88ea3bf Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/abstract_async_rest.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/abstract_async_websocket.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/abstract_async_websocket.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6aca3133c9cdd4001c3e2fab50613e98e4e4f6df Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/abstract_async_websocket.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/abstract_sync_rest.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/abstract_sync_rest.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..165c51f40cb5952c13936ca8b0f1528257059b8a Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/abstract_sync_rest.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/abstract_sync_websocket.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/abstract_sync_websocket.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0b8bba58fd3a22121d041dcd4a6bae7f68568c9 Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/abstract_sync_websocket.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/enums.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/enums.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ba37aaf71787544df16b4348dfa43235c4e6e32 Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/enums.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/errors.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/errors.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48526521151f5ab2cd9e5c77f49f201739b7429f Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/errors.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/helpers.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/helpers.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..331fe2556463c458a2324f83aeaf569fc77918b7 Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/helpers.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/options.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48c0b9abcddd0c7f770154188628f943e7267d6b Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/options.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/rest_response.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/rest_response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a93fb0aa67fe9bc3e51de77edb1024814d29ee04 Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/rest_response.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/shared_response.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/shared_response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7807848fdd2e19f56fc044ec284c4b4e3fd31b62 Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/shared_response.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/websocket_events.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/websocket_events.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..756e2e11aa427da3bbffaa242d06c3b1797cc7b5 Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/websocket_events.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/__pycache__/websocket_response.cpython-313.pyc b/deepgram/clients/common/v1/__pycache__/websocket_response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e9a09ea4d7433b15b0715616a360e0f94e565ea Binary files /dev/null and b/deepgram/clients/common/v1/__pycache__/websocket_response.cpython-313.pyc differ diff --git a/deepgram/clients/common/v1/abstract_async_rest.py b/deepgram/clients/common/v1/abstract_async_rest.py new file mode 100644 index 0000000000000000000000000000000000000000..3c0d8056028725d674553f5d25800eef729f53ab --- /dev/null +++ b/deepgram/clients/common/v1/abstract_async_rest.py @@ -0,0 +1,383 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import json +import io +from typing import Dict, Optional, List, Union + +import httpx + +from .helpers import append_query_params +from ....options import DeepgramClientOptions +from .errors import DeepgramError, DeepgramApiError, DeepgramUnknownApiError + + +class AbstractAsyncRestClient: + """ + An abstract base class for a RESTful HTTP client. + + This class provides common HTTP methods (GET, POST, PUT, PATCH, DELETE) for making asynchronous HTTP requests. + It handles error responses and provides basic JSON parsing. + + Args: + url (Dict): The base URL for the RESTful API, including any path segments. + headers (Optional[Dict[str, Any]]): Optional HTTP headers to include in requests. + params (Optional[Dict[str, Any]]): Optional query parameters to include in requests. + timeout (Optional[httpx.Timeout]): Optional timeout configuration for requests. + + Exceptions: + DeepgramApiError: Raised for known API errors. + DeepgramUnknownApiError: Raised for unknown API errors. + """ + + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + if config is None: + raise DeepgramError("Config are required") + self._config = config + + # pylint: disable=too-many-positional-arguments + + async def get( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a GET request to the specified URL. + """ + return await self._handle_request( + "GET", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + async def post_raw( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> httpx.Response: + """ + Make a POST request to the specified URL and return response in raw bytes. + """ + return await self._handle_request_raw( + "POST", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + async def post_memory( + self, + url: str, + file_result: List, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> Dict[str, Union[str, io.BytesIO]]: + """ + Make a POST request to the specified URL and return response in memory. + """ + return await self._handle_request_memory( + "POST", + url, + file_result=file_result, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + async def post( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a POST request to the specified URL. + """ + return await self._handle_request( + "POST", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + async def put( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a PUT request to the specified URL. + """ + return await self._handle_request( + "PUT", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + async def patch( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a PATCH request to the specified URL. + """ + return await self._handle_request( + "PATCH", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + async def delete( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a DELETE request to the specified URL. + """ + return await self._handle_request( + "DELETE", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + # pylint: disable-msg=too-many-locals,too-many-branches,too-many-locals + async def _handle_request( + self, + method: str, + url: str, + params: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + _url = url + if params is not None: + _url = append_query_params(_url, params) + if addons is not None: + _url = append_query_params(_url, addons) + _headers = self._config.headers + if headers is not None: + _headers.update(headers) + if timeout is None: + timeout = httpx.Timeout(30.0, connect=10.0) + + try: + transport = kwargs.get("transport") + async with httpx.AsyncClient( + timeout=timeout, transport=transport + ) as client: + if transport: + kwargs.pop("transport") + response = await client.request( + method, _url, headers=_headers, **kwargs + ) + response.raise_for_status() + + # throw exception if response is None or response.text is None + if response is None or response.text is None: + raise DeepgramError( + "Response is not available yet. Please try again later." + ) + + return response.text + + except httpx.HTTPError as e1: + if isinstance(e1, httpx.HTTPStatusError): + status_code = e1.response.status_code or 500 + try: + json_object = json.loads(e1.response.text) + raise DeepgramApiError( + json_object.get("err_msg"), + str(status_code), + json.dumps(json_object), + ) from e1 + except json.decoder.JSONDecodeError as e2: + raise DeepgramUnknownApiError(e2.msg, str(status_code)) from e2 + except ValueError as e2: + raise DeepgramUnknownApiError(str(e2), str(status_code)) from e2 + else: + raise # pylint: disable-msg=try-except-raise + except Exception: # pylint: disable-msg=try-except-raise + raise + + # pylint: enable-msg=too-many-locals,too-many-branches,too-many-locals + + # pylint: disable-msg=too-many-locals,too-many-branches + async def _handle_request_memory( + self, + method: str, + url: str, + file_result: List, + params: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> Dict[str, Union[str, io.BytesIO]]: + _url = url + if params is not None: + _url = append_query_params(_url, params) + if addons is not None: + _url = append_query_params(_url, addons) + _headers = self._config.headers + if headers is not None: + _headers.update(headers) + if timeout is None: + timeout = httpx.Timeout(30.0, connect=10.0) + + try: + transport = kwargs.get("transport") + async with httpx.AsyncClient( + timeout=timeout, transport=transport + ) as client: + if transport: + kwargs.pop("transport") + response = await client.request( + method, _url, headers=_headers, **kwargs + ) + response.raise_for_status() + + ret: Dict[str, Union[str, io.BytesIO]] = {} + for item in file_result: + if item in response.headers: + ret[item] = response.headers[item] + continue + tmp_item = f"dg-{item}" + if tmp_item in response.headers: + ret[item] = response.headers[tmp_item] + continue + tmp_item = f"x-dg-{item}" + if tmp_item in response.headers: + ret[item] = response.headers[tmp_item] + ret["stream"] = io.BytesIO(response.content) + return ret + + except httpx.HTTPError as e1: + if isinstance(e1, httpx.HTTPStatusError): + status_code = e1.response.status_code or 500 + try: + json_object = json.loads(e1.response.text) + raise DeepgramApiError( + json_object.get("err_msg"), + str(status_code), + json.dumps(json_object), + ) from e1 + except json.decoder.JSONDecodeError as e2: + raise DeepgramUnknownApiError(e2.msg, str(status_code)) from e2 + except ValueError as e2: + raise DeepgramUnknownApiError(str(e2), str(status_code)) from e2 + else: + raise # pylint: disable-msg=try-except-raise + except Exception: # pylint: disable-msg=try-except-raise + raise + + # pylint: enable-msg=too-many-locals,too-many-branches + + # pylint: disable-msg=too-many-locals,too-many-branches + async def _handle_request_raw( + self, + method: str, + url: str, + params: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> httpx.Response: + _url = url + if params is not None: + _url = append_query_params(_url, params) + if addons is not None: + _url = append_query_params(_url, addons) + _headers = self._config.headers + if headers is not None: + _headers.update(headers) + if timeout is None: + timeout = httpx.Timeout(30.0, connect=10.0) + + try: + transport = kwargs.get("transport") + client = httpx.AsyncClient(timeout=timeout, transport=transport) + if transport: + kwargs.pop("transport") + req = client.build_request(method, _url, headers=_headers, **kwargs) + return await client.send(req, stream=True) + + except httpx.HTTPError as e1: + if isinstance(e1, httpx.HTTPStatusError): + status_code = e1.response.status_code or 500 + try: + json_object = json.loads(e1.response.text) + raise DeepgramApiError( + json_object.get("err_msg"), + str(status_code), + json.dumps(json_object), + ) from e1 + except json.decoder.JSONDecodeError as e2: + raise DeepgramUnknownApiError(e2.msg, str(status_code)) from e2 + except ValueError as e2: + raise DeepgramUnknownApiError(str(e2), str(status_code)) from e2 + else: + raise # pylint: disable-msg=try-except-raise + except Exception: # pylint: disable-msg=try-except-raise + raise + + # pylint: enable-msg=too-many-locals,too-many-branches + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/common/v1/abstract_async_websocket.py b/deepgram/clients/common/v1/abstract_async_websocket.py new file mode 100644 index 0000000000000000000000000000000000000000..5f374ae36e2cfb925b5cd461460fea90239365fb --- /dev/null +++ b/deepgram/clients/common/v1/abstract_async_websocket.py @@ -0,0 +1,537 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT +import asyncio +import json +import logging +from typing import Dict, Union, Optional, cast, Any, Callable +from datetime import datetime +import threading +from abc import ABC, abstractmethod + +import websockets + +try: + # Websockets versions >= 13 + from websockets.asyncio.client import connect, ClientConnection + + WS_ADDITIONAL_HEADERS_KEY = "additional_headers" +except ImportError: + # Backward compatibility with websockets versions 12 + from websockets.legacy.client import ( # type: ignore + connect, + WebSocketClientProtocol as ClientConnection, + ) + + WS_ADDITIONAL_HEADERS_KEY = "extra_headers" + +from ....audio import Speaker +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from .helpers import convert_to_websocket_url, append_query_params +from .errors import DeepgramError + +from .websocket_response import ( + OpenResponse, + CloseResponse, + ErrorResponse, +) +from .websocket_events import WebSocketEvents + + +ONE_SECOND = 1 +HALF_SECOND = 0.5 +DEEPGRAM_INTERVAL = 5 +PING_INTERVAL = 20 + + +class AbstractAsyncWebSocketClient(ABC): # pylint: disable=too-many-instance-attributes + """ + Abstract class for using WebSockets. + + This class provides methods to establish a WebSocket connection generically for + use in all WebSocket clients. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + _websocket_url: str + + _socket: Optional[ClientConnection] = None + + _listen_thread: Union[asyncio.Task, None] + _delegate: Optional[Speaker] = None + + _kwargs: Optional[Dict] = None + _addons: Optional[Dict] = None + _options: Optional[Dict] = None + _headers: Optional[Dict] = None + + def __init__(self, config: DeepgramClientOptions, endpoint: str = ""): + if config is None: + raise DeepgramError("Config is required") + if endpoint == "": + raise DeepgramError("endpoint is required") + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + self._endpoint = endpoint + + self._listen_thread = None + + # events + self._exit_event = asyncio.Event() + + # set websocket url + self._websocket_url = convert_to_websocket_url(self._config.url, self._endpoint) + + def delegate_listening(self, delegate: Speaker) -> None: + """ + Delegate the listening thread to the Speaker object. + """ + self._delegate = delegate + + # pylint: disable=too-many-branches,too-many-statements + async def start( + self, + options: Optional[Any] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> bool: + """ + Starts the WebSocket connection for live transcription. + """ + self._logger.debug("AbstractAsyncWebSocketClient.start ENTER") + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + self._logger.info("kwargs: %s", kwargs) + + self._addons = addons + self._headers = headers + + # set kwargs + if kwargs is not None: + self._kwargs = kwargs + else: + self._kwargs = {} + + if not isinstance(options, dict): + self._logger.error("options is not a dict") + self._logger.debug("AbstractSyncWebSocketClient.start LEAVE") + return False + + # set options + if options is not None: + self._options = options + else: + self._options = {} + + combined_options = self._options.copy() + if self._addons is not None: + self._logger.info("merging addons to options") + combined_options.update(self._addons) + self._logger.info("new options: %s", combined_options) + self._logger.debug("combined_options: %s", combined_options) + + combined_headers = self._config.headers.copy() + if self._headers is not None: + self._logger.info("merging headers to options") + combined_headers.update(self._headers) + self._logger.info("new headers: %s", combined_headers) + self._logger.debug("combined_headers: %s", combined_headers) + + url_with_params = append_query_params(self._websocket_url, combined_options) + + try: + ws_connect_kwargs: Dict = { + "ping_interval": PING_INTERVAL, + WS_ADDITIONAL_HEADERS_KEY: combined_headers, + } + + self._socket = await connect( + url_with_params, + **ws_connect_kwargs, + ) + self._exit_event.clear() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # delegate the listening thread to external object + if self._delegate is not None: + self._logger.notice("_delegate is enabled. this is usually the speaker") + self._delegate.set_pull_callback(self._socket.recv) + self._delegate.set_push_callback(self._process_message) + else: + self._logger.notice("create _listening thread") + self._listen_thread = asyncio.create_task(self._listening()) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # push open event + await self._emit( + WebSocketEvents(WebSocketEvents.Open), + OpenResponse(type=WebSocketEvents.Open), + ) + + self._logger.notice("start succeeded") + self._logger.debug("AbstractAsyncWebSocketClient.start LEAVE") + return True + except websockets.exceptions.ConnectionClosed as e: + self._logger.error( + "ConnectionClosed in AbstractAsyncWebSocketClient.start: %s", e + ) + self._logger.debug("AbstractAsyncWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect", False): + raise + return False + except websockets.exceptions.WebSocketException as e: + self._logger.error( + "WebSocketException in AbstractAsyncWebSocketClient.start: %s", e + ) + self._logger.debug("AbstractAsyncWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect", False): + raise + return False + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "WebSocketException in AbstractAsyncWebSocketClient.start: %s", e + ) + self._logger.debug("AbstractAsyncWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect", False): + raise + return False + + async def is_connected(self) -> bool: + """ + Returns the connection status of the WebSocket. + """ + return self._socket is not None + + # pylint: enable=too-many-branches,too-many-statements + + @abstractmethod + def on(self, event: WebSocketEvents, handler: Callable) -> None: + """ + Registers an event handler for the WebSocket connection. + """ + raise NotImplementedError("no on method") + + @abstractmethod + async def _emit(self, event: WebSocketEvents, *args, **kwargs) -> None: + """ + Emits an event to the WebSocket connection. + """ + raise NotImplementedError("no _emit method") + + # pylint: disable=too-many-return-statements,too-many-statements,too-many-locals,too-many-branches + async def _listening(self) -> None: + """ + Listens for messages from the WebSocket connection. + """ + self._logger.debug("AbstractAsyncWebSocketClient._listening ENTER") + + while True: + try: + if self._exit_event.is_set(): + self._logger.notice("_listening exiting gracefully") + self._logger.debug("AbstractAsyncWebSocketClient._listening LEAVE") + return + + if self._socket is None: + self._logger.warning("socket is empty") + self._logger.debug("AbstractAsyncWebSocketClient._listening LEAVE") + return + + message = await self._socket.recv() + + if message is None: + self._logger.info("message is None") + continue + + self._logger.spam("data type: %s", type(message)) + + if isinstance(message, bytes): + self._logger.debug("Binary data received") + await self._process_binary(message) + else: + self._logger.debug("Text data received") + await self._process_text(message) + + self._logger.notice("_listening Succeeded") + self._logger.debug("AbstractAsyncWebSocketClient._listening LEAVE") + + except websockets.exceptions.ConnectionClosedOK as e: + # signal exit and close + await self._signal_exit() + + self._logger.notice(f"_listening({e.code}) exiting gracefully") + self._logger.debug("AbstractAsyncWebSocketClient._listening LEAVE") + return + + except websockets.exceptions.ConnectionClosed as e: + if e.code in [1000, 1001]: + # signal exit and close + await self._signal_exit() + + self._logger.notice(f"_listening({e.code}) exiting gracefully") + self._logger.debug("AbstractAsyncWebSocketClient._listening LEAVE") + return + + # we need to explicitly call self._signal_exit() here because we are hanging on a recv() + # note: this is different than the speak websocket client + self._logger.error( + "ConnectionClosed in AbstractAsyncWebSocketClient._listening with code %s: %s", + e.code, + e.reason, + ) + cc_error: ErrorResponse = ErrorResponse( + "ConnectionClosed in AbstractAsyncWebSocketClient._listening", + f"{e}", + "ConnectionClosed", + ) + await self._emit( + WebSocketEvents(WebSocketEvents.Error), + error=cc_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await self._signal_exit() + + self._logger.debug("AbstractAsyncWebSocketClient._listening LEAVE") + + if self._config.options.get("termination_exception_connect") is True: + raise + return + + except websockets.exceptions.WebSocketException as e: + self._logger.error( + "WebSocketException in AbstractAsyncWebSocketClient._listening: %s", + e, + ) + ws_error: ErrorResponse = ErrorResponse( + "WebSocketException in AbstractAsyncWebSocketClient._listening", + f"{e}", + "WebSocketException", + ) + await self._emit( + WebSocketEvents(WebSocketEvents.Error), + error=ws_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await self._signal_exit() + + self._logger.debug("AbstractAsyncWebSocketClient._listening LEAVE") + + if self._config.options.get("termination_exception_connect") is True: + raise + return + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AbstractAsyncWebSocketClient._listening: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AbstractAsyncWebSocketClient._listening", + f"{e}", + "Exception", + ) + await self._emit( + WebSocketEvents(WebSocketEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await self._signal_exit() + + self._logger.debug("AbstractAsyncWebSocketClient._listening LEAVE") + + if self._config.options.get("termination_exception_connect") is True: + raise + return + + # pylint: enable=too-many-return-statements,too-many-statements,too-many-locals,too-many-branches + + async def _process_message(self, message: Union[str, bytes]) -> None: + if isinstance(message, bytes): + await self._process_binary(message) + else: + await self._process_text(message) + + @abstractmethod + async def _process_text(self, message: str) -> None: + raise NotImplementedError("no _process_text method") + + @abstractmethod + async def _process_binary(self, message: bytes) -> None: + raise NotImplementedError("no _process_binary method") + + @abstractmethod + async def _close_message(self) -> bool: + raise NotImplementedError("no _close_message method") + + # pylint: disable=too-many-return-statements,too-many-branches + + async def send(self, data: Union[str, bytes]) -> bool: + """ + Sends data over the WebSocket connection. + """ + self._logger.spam("AbstractAsyncWebSocketClient.send ENTER") + + if self._exit_event.is_set(): + self._logger.notice("send exiting gracefully") + self._logger.debug("AbstractAsyncWebSocketClient.send LEAVE") + return False + + if not await self.is_connected(): + self._logger.notice("is_connected is False") + self._logger.debug("AbstractAsyncWebSocketClient.send LEAVE") + return False + + if self._socket is not None: + try: + await self._socket.send(data) + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.notice(f"send() exiting gracefully: {e.code}") + self._logger.debug("AbstractAsyncWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return True + except websockets.exceptions.ConnectionClosed as e: + if e.code in [1000, 1001]: + self._logger.notice(f"send({e.code}) exiting gracefully") + self._logger.debug("AbstractAsyncWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return True + + self._logger.error("send() failed - ConnectionClosed: %s", str(e)) + self._logger.spam("AbstractAsyncWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return False + except websockets.exceptions.WebSocketException as e: + self._logger.error("send() failed - WebSocketException: %s", str(e)) + self._logger.spam("AbstractAsyncWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return False + except Exception as e: # pylint: disable=broad-except + self._logger.error("send() failed - Exception: %s", str(e)) + self._logger.spam("AbstractAsyncWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return False + + self._logger.spam("send() succeeded") + self._logger.spam("AbstractAsyncWebSocketClient.send LEAVE") + return True + + self._logger.spam("send() failed. socket is None") + self._logger.spam("AbstractAsyncWebSocketClient.send LEAVE") + return False + + # pylint: enable=too-many-return-statements,too-many-branches + + async def finish(self) -> bool: + """ + Closes the WebSocket connection gracefully. + """ + self._logger.debug("AbstractAsyncWebSocketClient.finish ENTER") + + # signal exit + await self._signal_exit() + + # stop the threads + self._logger.verbose("cancelling tasks...") + try: + # Before cancelling, check if the tasks were created + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + tasks = [] + if self._listen_thread is not None: + self._listen_thread.cancel() + tasks.append(self._listen_thread) + self._logger.notice("processing _listen_thread cancel...") + + # Use asyncio.gather to wait for tasks to be cancelled + await asyncio.gather(*filter(None, tasks)) + self._logger.notice("threads joined") + + # debug the threads + for thread in threading.enumerate(): + if thread is not None and thread.name is not None: + self._logger.debug("after running thread: %s", thread.name) + else: + self._logger.debug("after running thread: unknown_thread_name") + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("finish succeeded") + self._logger.spam("AbstractAsyncWebSocketClient.finish LEAVE") + return True + + except asyncio.CancelledError as e: + self._logger.error("tasks cancelled error: %s", e) + self._logger.debug("AbstractAsyncWebSocketClient.finish LEAVE") + return True + + async def _signal_exit(self) -> None: + # send close event + self._logger.verbose("closing socket...") + if self._socket is not None: + self._logger.verbose("send Close...") + try: + # if the socket connection is closed, the following line might throw an error + await self._close_message() + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.notice("_signal_exit - ConnectionClosedOK: %s", e.code) + except websockets.exceptions.ConnectionClosed as e: + self._logger.error("_signal_exit - ConnectionClosed: %s", e.code) + except websockets.exceptions.WebSocketException as e: + self._logger.error("_signal_exit - WebSocketException: %s", str(e)) + except Exception as e: # pylint: disable=broad-except + self._logger.error("_signal_exit - Exception: %s", str(e)) + + # push close event + try: + await self._emit( + WebSocketEvents(WebSocketEvents.Close), + close=CloseResponse(type=WebSocketEvents.Close), + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + except Exception as e: # pylint: disable=broad-except + self._logger.error("_emit - Exception: %s", e) + + # wait for task to send + await asyncio.sleep(0.5) + + # signal exit + self._exit_event.set() + + # closes the WebSocket connection gracefully + self._logger.verbose("clean up socket...") + if self._socket is not None: + self._logger.verbose("socket.wait_closed...") + try: + await self._socket.close() + except websockets.exceptions.WebSocketException as e: + self._logger.error("socket.wait_closed failed: %s", e) + + self._socket = None diff --git a/deepgram/clients/common/v1/abstract_sync_rest.py b/deepgram/clients/common/v1/abstract_sync_rest.py new file mode 100644 index 0000000000000000000000000000000000000000..cbfd40ef20032c9d77f3ffc179fea15773bcedd0 --- /dev/null +++ b/deepgram/clients/common/v1/abstract_sync_rest.py @@ -0,0 +1,375 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import json +import io +from typing import Dict, Optional, List, Union + +import httpx + +from .helpers import append_query_params +from ....options import DeepgramClientOptions +from .errors import DeepgramError, DeepgramApiError, DeepgramUnknownApiError + + +class AbstractSyncRestClient: + """ + An abstract base class for a RESTful HTTP client. + + This class provides common HTTP methods (GET, POST, PUT, PATCH, DELETE) for making asynchronous HTTP requests. + It handles error responses and provides basic JSON parsing. + + Args: + url (Dict): The base URL for the RESTful API, including any path segments. + headers (Optional[Dict[str, Any]]): Optional HTTP headers to include in requests. + params (Optional[Dict[str, Any]]): Optional query parameters to include in requests. + timeout (Optional[httpx.Timeout]): Optional timeout configuration for requests. + + Exceptions: + DeepgramApiError: Raised for known API errors. + DeepgramUnknownApiError: Raised for unknown API errors. + """ + + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + if config is None: + raise DeepgramError("Config are required") + self._config = config + + # pylint: disable=too-many-positional-arguments + + def get( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a GET request to the specified URL. + """ + return self._handle_request( + "GET", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + def post_raw( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> httpx.Response: + """ + Make a POST request to the specified URL and return response in raw bytes. + """ + return self._handle_request_raw( + "POST", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + def post_memory( + self, + url: str, + file_result: List, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> Dict[str, Union[str, io.BytesIO]]: + """ + Make a POST request to the specified URL and return response in memory. + """ + return self._handle_request_memory( + "POST", + url, + file_result=file_result, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + def post( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a POST request to the specified URL. + """ + return self._handle_request( + "POST", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + def put( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a PUT request to the specified URL. + """ + return self._handle_request( + "PUT", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + def patch( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a PATCH request to the specified URL. + """ + return self._handle_request( + "PATCH", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + def delete( + self, + url: str, + options: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + """ + Make a DELETE request to the specified URL. + """ + return self._handle_request( + "DELETE", + url, + params=options, + addons=addons, + headers=headers, + timeout=timeout, + **kwargs, + ) + + # pylint: disable-msg=too-many-locals,too-many-branches + def _handle_request( + self, + method: str, + url: str, + params: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> str: + _url = url + if params is not None: + _url = append_query_params(_url, params) + if addons is not None: + _url = append_query_params(_url, addons) + _headers = self._config.headers + if headers is not None: + _headers.update(headers) + if timeout is None: + timeout = httpx.Timeout(30.0, connect=10.0) + + try: + transport = kwargs.get("transport") + with httpx.Client(timeout=timeout, transport=transport) as client: + if transport: + kwargs.pop("transport") + response = client.request(method, _url, headers=_headers, **kwargs) + response.raise_for_status() + + # throw exception if response is None or response.text is None + if response is None or response.text is None: + raise DeepgramError( + "Response is not available yet. Please try again later." + ) + + return response.text + + except httpx.HTTPError as e1: + if isinstance(e1, httpx.HTTPStatusError): + status_code = e1.response.status_code or 500 + try: + json_object = json.loads(e1.response.text) + raise DeepgramApiError( + json_object.get("err_msg"), + str(status_code), + json.dumps(json_object), + ) from e1 + except json.decoder.JSONDecodeError as e2: + raise DeepgramUnknownApiError(e2.msg, str(status_code)) from e2 + except ValueError as e2: + raise DeepgramUnknownApiError(str(e2), str(status_code)) from e2 + else: + raise # pylint: disable-msg=try-except-raise + except Exception: # pylint: disable-msg=try-except-raise + raise + + # pylint: enable-msg=too-many-locals,too-many-branches + + # pylint: disable-msg=too-many-branches,too-many-locals + def _handle_request_memory( + self, + method: str, + url: str, + file_result: List, + params: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> Dict[str, Union[str, io.BytesIO]]: + _url = url + if params is not None: + _url = append_query_params(_url, params) + if addons is not None: + _url = append_query_params(_url, addons) + _headers = self._config.headers + if headers is not None: + _headers.update(headers) + if timeout is None: + timeout = httpx.Timeout(30.0, connect=10.0) + + try: + transport = kwargs.get("transport") + with httpx.Client(timeout=timeout, transport=transport) as client: + if transport: + kwargs.pop("transport") + response = client.request(method, _url, headers=_headers, **kwargs) + response.raise_for_status() + + ret: Dict[str, Union[str, io.BytesIO]] = {} + for item in file_result: + if item in response.headers: + ret[item] = response.headers[item] + continue + tmp_item = f"dg-{item}" + if tmp_item in response.headers: + ret[item] = response.headers[tmp_item] + continue + tmp_item = f"x-dg-{item}" + if tmp_item in response.headers: + ret[item] = response.headers[tmp_item] + ret["stream"] = io.BytesIO(response.content) + return ret + + except httpx.HTTPError as e1: + if isinstance(e1, httpx.HTTPStatusError): + status_code = e1.response.status_code or 500 + try: + json_object = json.loads(e1.response.text) + raise DeepgramApiError( + json_object.get("err_msg"), + str(status_code), + json.dumps(json_object), + ) from e1 + except json.decoder.JSONDecodeError as e2: + raise DeepgramUnknownApiError(e2.msg, str(status_code)) from e2 + except ValueError as e2: + raise DeepgramUnknownApiError(str(e2), str(status_code)) from e2 + else: + raise # pylint: disable-msg=try-except-raise + except Exception: # pylint: disable-msg=try-except-raise + raise + + # pylint: disable-msg=too-many-branches,too-many-locals + + # pylint: disable-msg=too-many-branches,too-many-locals + def _handle_request_raw( + self, + method: str, + url: str, + params: Optional[Dict] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ) -> httpx.Response: + _url = url + if params is not None: + _url = append_query_params(_url, params) + if addons is not None: + _url = append_query_params(_url, addons) + _headers = self._config.headers + if headers is not None: + _headers.update(headers) + if timeout is None: + timeout = httpx.Timeout(30.0, connect=10.0) + + try: + transport = kwargs.get("transport") + client = httpx.Client(timeout=timeout, transport=transport) + if transport: + kwargs.pop("transport") + req = client.build_request(method, _url, headers=_headers, **kwargs) + return client.send(req, stream=True) + + except httpx.HTTPError as e1: + if isinstance(e1, httpx.HTTPStatusError): + status_code = e1.response.status_code or 500 + try: + json_object = json.loads(e1.response.text) + raise DeepgramApiError( + json_object.get("err_msg"), + str(status_code), + json.dumps(json_object), + ) from e1 + except json.decoder.JSONDecodeError as e2: + raise DeepgramUnknownApiError(e2.msg, str(status_code)) from e2 + except ValueError as e2: + raise DeepgramUnknownApiError(str(e2), str(status_code)) from e2 + else: + raise # pylint: disable-msg=try-except-raise + except Exception: # pylint: disable-msg=try-except-raise + raise + + # pylint: enable-msg=too-many-branches,too-many-locals + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/common/v1/abstract_sync_websocket.py b/deepgram/clients/common/v1/abstract_sync_websocket.py new file mode 100644 index 0000000000000000000000000000000000000000..23433a09791110923220eadd862687181c6fa83b --- /dev/null +++ b/deepgram/clients/common/v1/abstract_sync_websocket.py @@ -0,0 +1,527 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT +import json +import time +import logging +from typing import Dict, Union, Optional, cast, Any, Callable, Type +from datetime import datetime +import threading +from abc import ABC, abstractmethod + +from websockets.sync.client import connect, ClientConnection +import websockets + +from ....audio import Speaker +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from .helpers import convert_to_websocket_url, append_query_params +from .errors import DeepgramError + +from .websocket_response import ( + OpenResponse, + CloseResponse, + ErrorResponse, +) +from .websocket_events import WebSocketEvents + + +ONE_SECOND = 1 +HALF_SECOND = 0.5 +DEEPGRAM_INTERVAL = 5 +PING_INTERVAL = 20 + + +class AbstractSyncWebSocketClient(ABC): # pylint: disable=too-many-instance-attributes + """ + Abstract class for using WebSockets. + + This class provides methods to establish a WebSocket connection generically for + use in all WebSocket clients. + + Args: + config (DeepgramClientOptions): all the options for the client + endpoint (str): the endpoint to connect to + thread_cls (Type[threading.Thread]): optional thread class to use for creating threads, + defaults to threading.Thread. Useful for custom thread management like ContextVar support. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + _websocket_url: str + + _socket: Optional[ClientConnection] = None + _exit_event: threading.Event + _lock_send: threading.Lock + + _listen_thread: Union[threading.Thread, None] + _delegate: Optional[Speaker] = None + + _thread_cls: Type[threading.Thread] + + _kwargs: Optional[Dict] = None + _addons: Optional[Dict] = None + _options: Optional[Dict] = None + _headers: Optional[Dict] = None + + def __init__( + self, + config: DeepgramClientOptions, + endpoint: str = "", + thread_cls: Type[threading.Thread] = threading.Thread, + ): + if config is None: + raise DeepgramError("Config is required") + if endpoint == "": + raise DeepgramError("endpoint is required") + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + self._endpoint = endpoint + self._lock_send = threading.Lock() + + self._listen_thread = None + + self._thread_cls = thread_cls + + # exit + self._exit_event = threading.Event() + + # set websocket url + self._websocket_url = convert_to_websocket_url(self._config.url, self._endpoint) + + def delegate_listening(self, delegate: Speaker) -> None: + """ + Delegate the listening thread to the main thread. + """ + self._delegate = delegate + + # pylint: disable=too-many-statements,too-many-branches + def start( + self, + options: Optional[Any] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> bool: + """ + Starts the WebSocket connection for live transcription. + """ + self._logger.debug("AbstractSyncWebSocketClient.start ENTER") + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + self._logger.info("kwargs: %s", kwargs) + + self._addons = addons + self._headers = headers + + # set kwargs + if kwargs is not None: + self._kwargs = kwargs + else: + self._kwargs = {} + + if not isinstance(options, dict): + self._logger.error("options is not a dict") + self._logger.debug("AbstractSyncWebSocketClient.start LEAVE") + return False + + # set options + if options is not None: + self._options = options + else: + self._options = {} + + combined_options = self._options.copy() + if self._addons is not None: + self._logger.info("merging addons to options") + combined_options.update(self._addons) + self._logger.info("new options: %s", combined_options) + self._logger.debug("combined_options: %s", combined_options) + + combined_headers = self._config.headers.copy() + if self._headers is not None: + self._logger.info("merging headers to options") + combined_headers.update(self._headers) + self._logger.info("new headers: %s", combined_headers) + self._logger.debug("combined_headers: %s", combined_headers) + + url_with_params = append_query_params(self._websocket_url, combined_options) + try: + self._socket = connect(url_with_params, additional_headers=combined_headers) + self._exit_event.clear() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # delegate the listening thread to external object + if self._delegate is not None: + self._logger.notice("_delegate is enabled. this is usually the speaker") + self._delegate.set_pull_callback(self._socket.recv) + self._delegate.set_push_callback(self._process_message) + else: + self._logger.notice("create _listening thread") + self._listen_thread = self._thread_cls(target=self._listening) + self._listen_thread.start() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # push open event + self._emit( + WebSocketEvents(WebSocketEvents.Open), + OpenResponse(type=WebSocketEvents.Open), + ) + + self._logger.notice("start succeeded") + self._logger.debug("AbstractSyncWebSocketClient.start LEAVE") + return True + except websockets.exceptions.ConnectionClosed as e: + self._logger.error( + "ConnectionClosed in AbstractSyncWebSocketClient.start: %s", e + ) + self._logger.debug("AbstractSyncWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect", False): + raise e + return False + except websockets.exceptions.WebSocketException as e: + self._logger.error( + "WebSocketException in AbstractSyncWebSocketClient.start: %s", e + ) + self._logger.debug("AbstractSyncWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect", False): + raise e + return False + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "WebSocketException in AbstractSyncWebSocketClient.start: %s", e + ) + self._logger.debug("AbstractSyncWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect", False): + raise e + return False + + def is_connected(self) -> bool: + """ + Returns the connection status of the WebSocket. + """ + return self._socket is not None + + # pylint: enable=too-many-statements,too-many-branches + + @abstractmethod + def on(self, event: WebSocketEvents, handler: Callable) -> None: + """ + Registers an event handler for the WebSocket connection. + """ + raise NotImplementedError("no on method") + + @abstractmethod + def _emit(self, event: WebSocketEvents, *args, **kwargs) -> None: + """ + Emits an event to the WebSocket connection. + """ + raise NotImplementedError("no _emit method") + + # pylint: disable=too-many-return-statements,too-many-statements,too-many-locals,too-many-branches + def _listening( + self, + ) -> None: + """ + Listens for messages from the WebSocket connection. + """ + self._logger.debug("AbstractSyncWebSocketClient._listening ENTER") + + while True: + try: + if self._exit_event.is_set(): + self._logger.notice("_listening exiting gracefully") + self._logger.debug("AbstractSyncWebSocketClient._listening LEAVE") + return + + if self._socket is None: + self._logger.warning("socket is empty") + self._logger.debug("AbstractSyncWebSocketClient._listening LEAVE") + return + + message = self._socket.recv() + + if message is None: + self._logger.info("message is None") + continue + + self._logger.spam("data type: %s", type(message)) + + if isinstance(message, bytes): + self._logger.debug("Binary data received") + self._process_binary(message) + else: + self._logger.debug("Text data received") + self._process_text(message) + + self._logger.notice("_listening Succeeded") + self._logger.debug("AbstractSyncWebSocketClient._listening LEAVE") + + except websockets.exceptions.ConnectionClosedOK as e: + # signal exit and close + self._signal_exit() + + self._logger.notice(f"_listening({e.code}) exiting gracefully") + self._logger.debug("AbstractSyncWebSocketClient._listening LEAVE") + return + + except websockets.exceptions.ConnectionClosed as e: + if e.code in [1000, 1001]: + # signal exit and close + self._signal_exit() + + self._logger.notice(f"_listening({e.code}) exiting gracefully") + self._logger.debug("AbstractSyncWebSocketClient._listening LEAVE") + return + + # we need to explicitly call self._signal_exit() here because we are hanging on a recv() + # note: this is different than the speak websocket client + self._logger.error( + "ConnectionClosed in AbstractSyncWebSocketClient._listening with code %s: %s", + e.code, + e.reason, + ) + cc_error: ErrorResponse = ErrorResponse( + "ConnectionClosed in AbstractSyncWebSocketClient._listening", + f"{e}", + "ConnectionClosed", + ) + self._emit( + WebSocketEvents(WebSocketEvents.Error), + cc_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("AbstractSyncWebSocketClient._listening LEAVE") + + if self._config.options.get("termination_exception_connect") is True: + raise + return + + except websockets.exceptions.WebSocketException as e: + self._logger.error( + "WebSocketException in AbstractSyncWebSocketClient._listening with: %s", + e, + ) + ws_error: ErrorResponse = ErrorResponse( + "WebSocketException in AbstractSyncWebSocketClient._listening", + f"{e}", + "WebSocketException", + ) + self._emit( + WebSocketEvents(WebSocketEvents.Error), + ws_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("AbstractSyncWebSocketClient._listening LEAVE") + + if self._config.options.get("termination_exception_connect") is True: + raise + return + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AbstractSyncWebSocketClient._listening: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AbstractSyncWebSocketClient._listening", + f"{e}", + "Exception", + ) + self._emit( + WebSocketEvents(WebSocketEvents.Error), + e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("AbstractSyncWebSocketClient._listening LEAVE") + + if self._config.options.get("termination_exception_connect") is True: + raise + return + + # pylint: enable=too-many-return-statements,too-many-statements,too-many-locals,too-many-branches + + def _process_message(self, message: Union[str, bytes]) -> None: + if isinstance(message, bytes): + self._process_binary(message) + else: + self._process_text(message) + + @abstractmethod + def _process_text(self, message: str) -> None: + raise NotImplementedError("no _process_text method") + + @abstractmethod + def _process_binary(self, message: bytes) -> None: + raise NotImplementedError("no _process_binary method") + + @abstractmethod + def _close_message(self) -> bool: + raise NotImplementedError("no _close_message method") + + # pylint: disable=too-many-return-statements,too-many-branches + def send(self, data: Union[str, bytes]) -> bool: + """ + Sends data over the WebSocket connection. + """ + self._logger.spam("AbstractSyncWebSocketClient.send ENTER") + + if self._exit_event.is_set(): + self._logger.notice("send exiting gracefully") + self._logger.debug("AbstractSyncWebSocketClient.send LEAVE") + return False + + if not self.is_connected(): + self._logger.notice("is_connected is False") + self._logger.debug("AbstractSyncWebSocketClient.send LEAVE") + return False + + if self._socket is not None: + with self._lock_send: + try: + self._socket.send(data) + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.notice(f"send() exiting gracefully: {e.code}") + self._logger.debug("AbstractSyncWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return True + except websockets.exceptions.ConnectionClosed as e: + if e.code in [1000, 1001]: + self._logger.notice(f"send({e.code}) exiting gracefully") + self._logger.debug("AbstractSyncWebSocketClient.send LEAVE") + if ( + self._config.options.get("termination_exception_send") + == "true" + ): + raise + return True + self._logger.error("send() failed - ConnectionClosed: %s", str(e)) + self._logger.spam("AbstractSyncWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return False + except websockets.exceptions.WebSocketException as e: + self._logger.error("send() failed - WebSocketException: %s", str(e)) + self._logger.spam("AbstractSyncWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return False + except Exception as e: # pylint: disable=broad-except + self._logger.error("send() failed - Exception: %s", str(e)) + self._logger.spam("AbstractSyncWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return False + + self._logger.spam("send() succeeded") + self._logger.spam("AbstractSyncWebSocketClient.send LEAVE") + return True + + self._logger.spam("send() failed. socket is None") + self._logger.spam("AbstractSyncWebSocketClient.send LEAVE") + return False + + # pylint: enable=too-many-return-statements,too-many-branches + + def finish(self) -> bool: + """ + Closes the WebSocket connection gracefully. + """ + self._logger.spam("AbstractSyncWebSocketClient.finish ENTER") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # signal exit + self._signal_exit() + + # stop the threads + self._logger.verbose("cancelling tasks...") + if self._listen_thread is not None: + self._listen_thread.join() + self._listen_thread = None + self._logger.notice("listening thread joined") + + # debug the threads + for thread in threading.enumerate(): + if thread is not None and thread.name is not None: + self._logger.debug("before running thread: %s", thread.name) + else: + self._logger.debug("after running thread: unknown_thread_name") + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("finish succeeded") + self._logger.spam("AbstractSyncWebSocketClient.finish LEAVE") + return True + + # signals the WebSocket connection to exit + def _signal_exit(self) -> None: + # closes the WebSocket connection gracefully + self._logger.notice("closing socket...") + if self._socket is not None: + self._logger.notice("sending Close...") + try: + # if the socket connection is closed, the following line might throw an error + self._close_message() + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.notice("_signal_exit - ConnectionClosedOK: %s", e.code) + except websockets.exceptions.ConnectionClosed as e: + self._logger.error("_signal_exit - ConnectionClosed: %s", e.code) + except websockets.exceptions.WebSocketException as e: + self._logger.error("_signal_exit - WebSocketException: %s", str(e)) + except Exception as e: # pylint: disable=broad-except + self._logger.error("_signal_exit - Exception: %s", str(e)) + + # push close event + try: + self._emit( + WebSocketEvents(WebSocketEvents.Close), + CloseResponse(type=WebSocketEvents.Close), + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + except Exception as e: # pylint: disable=broad-except + self._logger.error("_signal_exit - Exception: %s", e) + + # wait for task to send + time.sleep(0.5) + + # signal exit + self._exit_event.set() + + # closes the WebSocket connection gracefully + self._logger.verbose("clean up socket...") + if self._socket is not None: + self._logger.verbose("socket.wait_closed...") + try: + self._socket.close() + except websockets.exceptions.WebSocketException as e: + self._logger.error("socket.wait_closed failed: %s", e) + + self._socket = None diff --git a/deepgram/clients/common/v1/enums.py b/deepgram/clients/common/v1/enums.py new file mode 100644 index 0000000000000000000000000000000000000000..a25893e9db83bb0e35df36726e5002410fa99eba --- /dev/null +++ b/deepgram/clients/common/v1/enums.py @@ -0,0 +1,18 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from aenum import StrEnum + +# Constants mapping to events from the Deepgram API + + +class Sentiment(StrEnum): + """ + Sentiment values. + """ + + UNKNOWN: str = "" + NEGATIVE: str = "negative" + NEUTRAL: str = "neutral" + POSITIVE: str = "positive" diff --git a/deepgram/clients/common/v1/errors.py b/deepgram/clients/common/v1/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..e0b5ac8e91b220c49ca95f21b81d8f59cc38a092 --- /dev/null +++ b/deepgram/clients/common/v1/errors.py @@ -0,0 +1,77 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + + +class DeepgramError(Exception): + """ + Exception raised for unknown errors related to the Deepgram API. + + Attributes: + message (str): The error message describing the exception. + """ + + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" + + +class DeepgramTypeError(Exception): + """ + Exception raised for unknown errors related to unknown Types for Transcription. + + Attributes: + message (str): The error message describing the exception. + """ + + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramTypeError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" + + +class DeepgramApiError(Exception): + """ + Exception raised for known errors (in json response format) related to the Deepgram API. + + Attributes: + message (str): The error message describing the exception. + status (str): The HTTP status associated with the API error. + original_error (str - json): The original error that was raised. + """ + + def __init__(self, message: str, status: str, original_error=None): + super().__init__(message) + self.name = "DeepgramApiError" + self.status = status + self.message = message + self.original_error = original_error + + def __str__(self): + return f"{self.name}: {self.message} (Status: {self.status})" + + +class DeepgramUnknownApiError(DeepgramApiError): + """ + Exception raised for unknown errors related to the Deepgram API. + + Attributes: + message (str): The error message describing the exception. + status (str): The HTTP status associated with the API error. + """ + + def __init__(self, message: str, status: str): + super().__init__(message, status) + self.name = "DeepgramUnknownApiError" + self.status = status + self.message = message + + def __str__(self): + return f"{self.name}: {self.message} (Status: {self.status})" diff --git a/deepgram/clients/common/v1/helpers.py b/deepgram/clients/common/v1/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..c7429acd5cb5e67edde5d62b870f4d3827315ee3 --- /dev/null +++ b/deepgram/clients/common/v1/helpers.py @@ -0,0 +1,53 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode +from typing import Dict, Optional +import re + + +# This function appends query parameters to a URL +def append_query_params(url: str, params: Optional[Dict] = None): + """ + Appends query parameters to a URL + """ + parsed_url = urlparse(url) + query_params = parse_qs(parsed_url.query) + + if params is not None: + for key, value in params.items(): + if value is None: + continue + if isinstance(value, bool): + value = str(value).lower() + if isinstance(value, list): + for item in value: + query_params[key] = query_params.get(key, []) + [str(item)] + else: + query_params[key] = [str(value)] + + updated_query_string = urlencode(query_params, doseq=True) + updated_url = parsed_url._replace(query=updated_query_string).geturl() + return updated_url + + +# This function converts a URL to a WebSocket URL +def convert_to_websocket_url(base_url: str, endpoint: str): + """ + Converts a URL to a WebSocket URL + """ + use_ssl = True # Default to true + if re.match(r"^https?://", base_url, re.IGNORECASE): + if "http://" in base_url: + use_ssl = False # Override to false if http:// is found + base_url = base_url.replace("https://", "").replace("http://", "") + if not re.match(r"^wss?://", base_url, re.IGNORECASE): + if use_ssl: + base_url = "wss://" + base_url + else: + base_url = "ws://" + base_url + parsed_url = urlparse(base_url) + domain = parsed_url.netloc + websocket_url = urlunparse((parsed_url.scheme, domain, endpoint, "", "", "")) + return websocket_url diff --git a/deepgram/clients/common/v1/options.py b/deepgram/clients/common/v1/options.py new file mode 100644 index 0000000000000000000000000000000000000000..c2db15d97119867c416705d4f798039a0ac7e0d1 --- /dev/null +++ b/deepgram/clients/common/v1/options.py @@ -0,0 +1,65 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from io import BufferedReader +from typing import Union +from typing_extensions import TypedDict + + +class StreamSource(TypedDict): + """ + Represents a data source for reading binary data from a stream-like source. + + This class is used to specify a source of binary data that can be read from + a stream, such as an audio file in .wav format. + + Attributes: + stream (BufferedReader): A BufferedReader object for reading binary data. + """ + + stream: BufferedReader + + +class UrlSource(TypedDict): + """ + Represents a data source for specifying the location of a file via a URL. + + This class is used to specify a hosted file URL, typically pointing to an + externally hosted file, such as an audio file hosted on a server or the internet. + + Attributes: + url (str): The URL pointing to the hosted file. + """ + + url: str + + +class BufferSource(TypedDict): + """ + Represents a data source for handling raw binary data. + + This class is used to specify raw binary data, such as audio data in its + binary form, which can be captured from a microphone or generated synthetically. + + Attributes: + buffer (bytes): The binary data. + """ + + buffer: bytes + + +class TextSource(TypedDict): + """ + Represents a data source for reading binary data from a text-like source. + + This class is used to specify a source of text data that can be read from. + + Attributes: + text (str): A string for reading text data. + """ + + text: str + + +FileSource = Union[TextSource, BufferSource, StreamSource] diff --git a/deepgram/clients/common/v1/rest_response.py b/deepgram/clients/common/v1/rest_response.py new file mode 100644 index 0000000000000000000000000000000000000000..22ca09eb6f6817992f6fefe91f45211abe1ff1dc --- /dev/null +++ b/deepgram/clients/common/v1/rest_response.py @@ -0,0 +1,181 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Dict, Any + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from .enums import Sentiment +from .shared_response import BaseResponse + + +# Analyze Response Types: + + +@dataclass +class IntentsInfo(BaseResponse): + """ + Intents Info + """ + + model_uuid: str = "" + input_tokens: int = 0 + output_tokens: int = 0 + + +@dataclass +class SentimentInfo(BaseResponse): + """ + Sentiment Info + """ + + model_uuid: str = "" + input_tokens: int = 0 + output_tokens: int = 0 + + +@dataclass +class SummaryInfo(BaseResponse): + """ + Summary Info + """ + + model_uuid: str = "" + input_tokens: int = 0 + output_tokens: int = 0 + + +@dataclass +class TopicsInfo(BaseResponse): + """ + Topics Info + """ + + model_uuid: str = "" + input_tokens: int = 0 + output_tokens: int = 0 + + +@dataclass +class Average(BaseResponse): + """ + Average + """ + + sentiment: Sentiment + sentiment_score: float = 0 + + def __getitem__(self, key): + _dict = self.to_dict() + if "sentiment" in _dict: + _dict["sentiment"] = Sentiment.from_dict(_dict["sentiment"]) + return _dict[key] + + +@dataclass +class Topic(BaseResponse): + """ + Topic + """ + + topic: str = "" + confidence_score: float = 0 + + +@dataclass +class Intent(BaseResponse): + """ + Intent + """ + + intent: str = "" + confidence_score: float = 0 + + +@dataclass +class Segment(BaseResponse): + """ + Segment + """ + + text: str = "" + start_word: int = 0 + end_word: int = 0 + sentiment: Optional[Sentiment] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment_score: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + intents: Optional[List[Intent]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + topics: Optional[List[Topic]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "sentiment" in _dict: + _dict["sentiment"] = Sentiment.from_dict(_dict["sentiment"]) + if "intents" in _dict: + _dict["intents"] = Intent.from_dict(_dict["intents"]) + if "topics" in _dict: + _dict["topics"] = Topic.from_dict(_dict["topics"]) + return _dict[key] + + +@dataclass +class Sentiments(BaseResponse): + """ + Sentiments + """ + + average: Average + segments: List[Segment] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "segments" in _dict: + _dict["segments"] = [ + Segment.from_dict(segments) for segments in _dict["segments"] + ] + if "average" in _dict: + _dict["average"] = Average.from_dict(_dict["average"]) + return _dict[key] + + +@dataclass +class Topics(BaseResponse): + """ + Topics + """ + + segments: List[Segment] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "segments" in _dict: + _dict["segments"] = [ + Segment.from_dict(segments) for segments in _dict["segments"] + ] + return _dict[key] + + +@dataclass +class Intents(BaseResponse): + """ + Intents + """ + + segments: List[Segment] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "segments" in _dict: + _dict["segments"] = [ + Segment.from_dict(segments) for segments in _dict["segments"] + ] + return _dict[key] diff --git a/deepgram/clients/common/v1/shared_response.py b/deepgram/clients/common/v1/shared_response.py new file mode 100644 index 0000000000000000000000000000000000000000..5342380eb923633105c5599871bdc9e1513b3964 --- /dev/null +++ b/deepgram/clients/common/v1/shared_response.py @@ -0,0 +1,86 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Dict, Any + + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config, DataClassJsonMixin + + +# base class + + +@dataclass +class BaseResponse(DataClassJsonMixin): + """ + BaseResponse class used to define the common methods and properties for all response classes. + """ + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + def __str__(self) -> str: + return self.to_json(indent=4) + + def eval(self, key: str) -> str: + """ + This method is used to evaluate a key in the response object using a dot notation style method. + """ + keys = key.split(".") + result: Dict[Any, Any] = self.to_dict() + for k in keys: + if isinstance(result, dict) and k in result: + result = result[k] + elif isinstance(result, list) and k.isdigit() and int(k) < len(result): + result = result[int(k)] + else: + return "" + return str(result) + + +# shared classes + + +@dataclass +class ModelInfo(BaseResponse): + """ + ModelInfo object + """ + + name: str = "" + version: str = "" + arch: str = "" + + +@dataclass +class Hit(BaseResponse): + """ + The hit information for the response. + """ + + confidence: float = 0 + start: float = 0 + end: float = 0 + snippet: Optional[str] = "" + + +@dataclass +class Search(BaseResponse): + """ + The search information for the response. + """ + + query: str = "" + hits: List[Hit] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "hits" in _dict: + _dict["hits"] = [Hit.from_dict(hits) for hits in _dict["hits"]] + return _dict[key] diff --git a/deepgram/clients/common/v1/websocket_events.py b/deepgram/clients/common/v1/websocket_events.py new file mode 100644 index 0000000000000000000000000000000000000000..4ab36a1634f463b8f7ac33867116345fe3856949 --- /dev/null +++ b/deepgram/clients/common/v1/websocket_events.py @@ -0,0 +1,19 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from aenum import StrEnum + +# Constants mapping to events from the Deepgram API + + +class WebSocketEvents(StrEnum): + """ + Enumerates the possible events that can be received from the Deepgram API + """ + + Open: str = "Open" + Close: str = "Close" + Warning: str = "Warning" + Error: str = "Error" + Unhandled: str = "Unhandled" diff --git a/deepgram/clients/common/v1/websocket_response.py b/deepgram/clients/common/v1/websocket_response.py new file mode 100644 index 0000000000000000000000000000000000000000..63d7c016da47ca272d2421aa4b33393a1b442944 --- /dev/null +++ b/deepgram/clients/common/v1/websocket_response.py @@ -0,0 +1,64 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Dict, Any + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from .shared_response import BaseResponse + + +# Result Message + + +@dataclass +class OpenResponse(BaseResponse): + """ + Open Message from the Deepgram Platform + """ + + type: str = "" + + +# Close Message + + +@dataclass +class CloseResponse(BaseResponse): + """ + Close Message from the Deepgram Platform + """ + + type: str = "" + + +# Error Message + + +@dataclass +class ErrorResponse(BaseResponse): + """ + Error Message from the Deepgram Platform + """ + + description: str = "" + message: str = "" + type: str = "" + variant: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + +# Unhandled Message + + +@dataclass +class UnhandledResponse(BaseResponse): + """ + Unhandled Message from the Deepgram Platform + """ + + type: str = "" + raw: str = "" diff --git a/deepgram/clients/errors.py b/deepgram/clients/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..197b3c7ed3cd24c03330e9896f3f7f8ef09f0d6b --- /dev/null +++ b/deepgram/clients/errors.py @@ -0,0 +1,16 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + + +class DeepgramModuleError(Exception): + """ + Base class for exceptions raised for a missing Deepgram module. + + Attributes: + message (str): The error message describing the exception. + """ + + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramModuleError" diff --git a/deepgram/clients/listen/__init__.py b/deepgram/clients/listen/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3a3285f594f8ed13176388a62cc59c4c8e2d9bfe --- /dev/null +++ b/deepgram/clients/listen/__init__.py @@ -0,0 +1,108 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .enums import LiveTranscriptionEvents + +# backward compat +from .client import ( + PreRecordedClient, + AsyncPreRecordedClient, + LiveClient, + AsyncLiveClient, +) + +# rest +# common +from .client import ( + UrlSource, + TextSource, + BufferSource, + StreamSource, + FileSource, +) + +## input +from .client import ( + ListenRESTOptions, + PrerecordedOptions, + PreRecordedStreamSource, + PrerecordedSource, + ListenRestSource, +) + +## output +from .client import ( + # top level + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, + # shared + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, + # between rest and websocket + ModelInfo, + Hit, + Search, + # unique + Entity, + ListenRESTMetadata, + Paragraph, + Paragraphs, + ListenRESTResults, + Sentence, + Summaries, + SummaryV1, + SummaryV2, + Translation, + Utterance, + Warning, + ListenRESTAlternative, + ListenRESTChannel, + ListenRESTWord, +) + + +# websocket +## input +from .client import ( + ListenWebSocketOptions, + LiveOptions, +) + +## output +from .client import ( + # top level + LiveResultResponse, + ListenWSMetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + # common websocket response + OpenResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, + # unique + ListenWSMetadata, + ListenWSWord, + ListenWSAlternative, + ListenWSChannel, +) + +# clients +from .client import ( + ListenRESTClient, + AsyncListenRESTClient, + ListenWebSocketClient, + AsyncListenWebSocketClient, +) diff --git a/deepgram/clients/listen/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/listen/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98c9ee29daf22d698fa5f58bd867d268d8d99389 Binary files /dev/null and b/deepgram/clients/listen/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/listen/__pycache__/client.cpython-313.pyc b/deepgram/clients/listen/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83fe42efb146c76816b27fc072de051b4aef1375 Binary files /dev/null and b/deepgram/clients/listen/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/listen/__pycache__/enums.cpython-313.pyc b/deepgram/clients/listen/__pycache__/enums.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef5f37514a7f0cc1d788baa9a7c63589e0a39d9c Binary files /dev/null and b/deepgram/clients/listen/__pycache__/enums.cpython-313.pyc differ diff --git a/deepgram/clients/listen/client.py b/deepgram/clients/listen/client.py new file mode 100644 index 0000000000000000000000000000000000000000..29b61bed6c1c1990fb43edca428978fa06808aa5 --- /dev/null +++ b/deepgram/clients/listen/client.py @@ -0,0 +1,180 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +# rest +from .v1 import ( + ListenRESTClient as ListenRESTClientLatest, + AsyncListenRESTClient as AsyncListenRESTClientLatest, +) +from .v1 import ( + PrerecordedOptions as PrerecordedOptionsLatest, + ListenRESTOptions as ListenRESTOptionsLatest, +) + +from .v1 import ( + UrlSource as UrlSourceLatest, + TextSource as TextSourceLatest, + BufferSource as BufferSourceLatest, + StreamSource as StreamSourceLatest, + FileSource as FileSourceLatest, + PreRecordedStreamSource as PreRecordedStreamSourceLatest, + PrerecordedSource as PrerecordedSourceLatest, + ListenRestSource as ListenRestSourceLatest, +) +from .v1 import ( + AsyncPrerecordedResponse as AsyncPrerecordedResponseLatest, + PrerecordedResponse as PrerecordedResponseLatest, + SyncPrerecordedResponse as SyncPrerecordedResponseLatest, + # shared + Average as AverageLatest, + Intent as IntentLatest, + Intents as IntentsLatest, + IntentsInfo as IntentsInfoLatest, + Segment as SegmentLatest, + SentimentInfo as SentimentInfoLatest, + Sentiment as SentimentLatest, + Sentiments as SentimentsLatest, + SummaryInfo as SummaryInfoLatest, + Topic as TopicLatest, + Topics as TopicsLatest, + TopicsInfo as TopicsInfoLatest, + # between rest and websocket + ModelInfo as ModelInfoLatest, + Hit as HitLatest, + Search as SearchLatest, + # unique + ListenRESTMetadata as ListenRESTMetadataLatest, + Entity as EntityLatest, + Paragraph as ParagraphLatest, + Paragraphs as ParagraphsLatest, + ListenRESTResults as ListenRESTResultsLatest, + Sentence as SentenceLatest, + Summaries as SummariesLatest, + SummaryV1 as SummaryV1Latest, + SummaryV2 as SummaryV2Latest, + Translation as TranslationLatest, + Utterance as UtteranceLatest, + Warning as WarningLatest, + ListenRESTAlternative as ListenRESTAlternativeLatest, + ListenRESTChannel as ListenRESTChannelLatest, + ListenRESTWord as ListenRESTWordLatest, +) + +# websocket +from .v1 import ( + ListenWebSocketClient as ListenWebSocketClientLatest, + AsyncListenWebSocketClient as AsyncListenWebSocketClientLatest, +) +from .v1 import ( + LiveOptions as LiveOptionsLatest, + ListenWebSocketOptions as ListenWebSocketOptionsLatest, +) +from .v1 import ( + OpenResponse as OpenResponseLatest, + LiveResultResponse as LiveResultResponseLatest, + ListenWSMetadataResponse as ListenWSMetadataResponseLatest, + SpeechStartedResponse as SpeechStartedResponseLatest, + UtteranceEndResponse as UtteranceEndResponseLatest, + CloseResponse as CloseResponseLatest, + ErrorResponse as ErrorResponseLatest, + UnhandledResponse as UnhandledResponseLatest, + ListenWSMetadata as ListenWSMetadataLatest, + ListenWSAlternative as ListenWSAlternativeLatest, + ListenWSChannel as ListenWSChannelLatest, + ListenWSWord as ListenWSWordLatest, +) + +# The vX/client.py points to the current supported version in the SDK. +# Older versions are supported in the SDK for backwards compatibility. + +# shared +Average = AverageLatest +Intent = IntentLatest +Intents = IntentsLatest +IntentsInfo = IntentsInfoLatest +Segment = SegmentLatest +SentimentInfo = SentimentInfoLatest +Sentiment = SentimentLatest +Sentiments = SentimentsLatest +SummaryInfo = SummaryInfoLatest +Topic = TopicLatest +Topics = TopicsLatest +TopicsInfo = TopicsInfoLatest + +# between rest and websocket +Hit = HitLatest +ModelInfo = ModelInfoLatest +Search = SearchLatest + +# websocket common +OpenResponse = OpenResponseLatest +CloseResponse = CloseResponseLatest +ErrorResponse = ErrorResponseLatest +UnhandledResponse = UnhandledResponseLatest + + +# backward compat +PreRecordedClient = ListenRESTClientLatest +AsyncPreRecordedClient = AsyncListenRESTClientLatest +LiveClient = ListenWebSocketClientLatest +AsyncLiveClient = ListenWebSocketClientLatest + +# rest +## common +UrlSource = UrlSourceLatest +TextSource = TextSourceLatest +BufferSource = BufferSourceLatest +StreamSource = StreamSourceLatest +FileSource = FileSourceLatest + +# input +ListenRESTOptions = ListenRESTOptionsLatest +PrerecordedOptions = PrerecordedOptionsLatest +PreRecordedStreamSource = PreRecordedStreamSourceLatest +PrerecordedSource = PrerecordedSourceLatest +ListenRestSource = ListenRestSourceLatest + +## output +AsyncPrerecordedResponse = AsyncPrerecordedResponseLatest +PrerecordedResponse = PrerecordedResponseLatest +SyncPrerecordedResponse = SyncPrerecordedResponseLatest +# unique +Entity = EntityLatest +ListenRESTMetadata = ListenRESTMetadataLatest +Paragraph = ParagraphLatest +Paragraphs = ParagraphsLatest +ListenRESTResults = ListenRESTResultsLatest +Sentence = SentenceLatest +Summaries = SummariesLatest +SummaryV1 = SummaryV1Latest +SummaryV2 = SummaryV2Latest +Translation = TranslationLatest +Utterance = UtteranceLatest +Warning = WarningLatest +ListenRESTAlternative = ListenRESTAlternativeLatest +ListenRESTChannel = ListenRESTChannelLatest +ListenRESTWord = ListenRESTWordLatest + +# websocket +## input +ListenWebSocketOptions = ListenWebSocketOptionsLatest +LiveOptions = LiveOptionsLatest + +## output +LiveResultResponse = LiveResultResponseLatest +ListenWSMetadataResponse = ListenWSMetadataResponseLatest +SpeechStartedResponse = SpeechStartedResponseLatest +UtteranceEndResponse = UtteranceEndResponseLatest + +## unique +ListenWSMetadata = ListenWSMetadataLatest +ListenWSAlternative = ListenWSAlternativeLatest +ListenWSChannel = ListenWSChannelLatest +ListenWSWord = ListenWSWordLatest + +# clients +ListenRESTClient = ListenRESTClientLatest +AsyncListenRESTClient = AsyncListenRESTClientLatest +ListenWebSocketClient = ListenWebSocketClientLatest +AsyncListenWebSocketClient = AsyncListenWebSocketClientLatest diff --git a/deepgram/clients/listen/enums.py b/deepgram/clients/listen/enums.py new file mode 100644 index 0000000000000000000000000000000000000000..8e39ae027dcf1303c87715efff00ba2e05d8a154 --- /dev/null +++ b/deepgram/clients/listen/enums.py @@ -0,0 +1,24 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from aenum import StrEnum + +# Constants mapping to events from the Deepgram API + + +class LiveTranscriptionEvents(StrEnum): + """ + Enumerates the possible events that can be received from the Deepgram API + """ + + Open: str = "Open" + Close: str = "Close" + Transcript: str = "Results" + Metadata: str = "Metadata" + UtteranceEnd: str = "UtteranceEnd" + SpeechStarted: str = "SpeechStarted" + Finalize: str = "Finalize" + Error: str = "Error" + Unhandled: str = "Unhandled" + Warning: str = "Warning" diff --git a/deepgram/clients/listen/v1/__init__.py b/deepgram/clients/listen/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d1b2f3a6e957b11cd2abf5c9237612a9952ff472 --- /dev/null +++ b/deepgram/clients/listen/v1/__init__.py @@ -0,0 +1,131 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +# backward compat +from .rest import ( + ListenRESTClient as PreRecordedClient, + AsyncListenRESTClient as AsyncPreRecordedClient, +) +from .websocket import ( + ListenWebSocketClient as LiveClient, + AsyncListenWebSocketClient as AsyncLiveClient, +) + +# shared +from ...common import ( + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, +) + +# between rest and websocket +from ...common import ( + ModelInfo, + Hit, + Search, +) + +# common websocket +from ...common import ( + OpenResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) + +# rest +from .rest import ListenRESTClient, AsyncListenRESTClient +from .rest import ListenRESTOptions, PrerecordedOptions +from .rest import ( + # common + UrlSource, + BufferSource, + StreamSource, + TextSource, + FileSource, + # unique + PreRecordedStreamSource, + PrerecordedSource, + ListenRestSource, +) +from .rest import ( + #### top level + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, + #### shared + # Average, + # Intent, + # Intents, + # IntentsInfo, + # Segment, + # SentimentInfo, + # Sentiment, + # Sentiments, + # SummaryInfo, + # Topic, + # Topics, + # TopicsInfo, + #### between rest and websocket + # ModelInfo, + # Alternative, + # Hit, + # Search, + # Channel, + # Word, + #### unique + Entity, + Metadata as ListenRESTMetadata, + Paragraph, + Paragraphs, + Results as ListenRESTResults, + Sentence, + Summaries, + SummaryV1, + SummaryV2, + Translation, + Utterance, + Warning, + ListenRESTAlternative, + ListenRESTChannel, + ListenRESTWord, +) + +# websocket +from .websocket import ListenWebSocketClient, AsyncListenWebSocketClient +from .websocket import LiveOptions, ListenWebSocketOptions +from .websocket import ( + #### top level + LiveResultResponse, + MetadataResponse as ListenWSMetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + #### common websocket response + # BaseResponse, + # OpenResponse, + # CloseResponse, + # ErrorResponse, + # UnhandledResponse, + #### between rest and websocket + # ModelInfo, + # Alternative, + # Hit, + # Search, + # Channel, + # Word, + #### unique + Metadata as ListenWSMetadata, + ListenWSWord, + ListenWSAlternative, + ListenWSChannel, +) diff --git a/deepgram/clients/listen/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/listen/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6dd35eda109ad5ae0b018347f3d6c36fb334e78e Binary files /dev/null and b/deepgram/clients/listen/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/rest/__init__.py b/deepgram/clients/listen/v1/rest/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8106a2dd7a8b5ff3db3a4e07430a748814425a4e --- /dev/null +++ b/deepgram/clients/listen/v1/rest/__init__.py @@ -0,0 +1,59 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import ListenRESTClient +from .async_client import AsyncListenRESTClient +from .options import ( + ListenRESTOptions, + PrerecordedOptions, + # common + UrlSource, + BufferSource, + StreamSource, + TextSource, + FileSource, + # unique + PreRecordedStreamSource, + PrerecordedSource, + ListenRestSource, +) +from .response import ( + # top level + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, + # shared + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, + # between rest and websocket + ModelInfo, + Hit, + Search, + # unique + Entity, + Metadata, + Paragraph, + Paragraphs, + Results, + Sentence, + Summaries, + SummaryV1, + SummaryV2, + Translation, + Utterance, + Warning, + ListenRESTAlternative, + ListenRESTChannel, + ListenRESTWord, +) diff --git a/deepgram/clients/listen/v1/rest/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/listen/v1/rest/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4945febc940424c808caa30ea158254d607a0bb Binary files /dev/null and b/deepgram/clients/listen/v1/rest/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/rest/__pycache__/async_client.cpython-313.pyc b/deepgram/clients/listen/v1/rest/__pycache__/async_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7cc35cd95d4d68b5622041061c037906b438f82c Binary files /dev/null and b/deepgram/clients/listen/v1/rest/__pycache__/async_client.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/rest/__pycache__/client.cpython-313.pyc b/deepgram/clients/listen/v1/rest/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cc8fd58a8d3639b7bbad17f413682351a03c86f Binary files /dev/null and b/deepgram/clients/listen/v1/rest/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/rest/__pycache__/helpers.cpython-313.pyc b/deepgram/clients/listen/v1/rest/__pycache__/helpers.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..807f5cf533f1ac66399a3e506a7f962c796edcdf Binary files /dev/null and b/deepgram/clients/listen/v1/rest/__pycache__/helpers.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/rest/__pycache__/options.cpython-313.pyc b/deepgram/clients/listen/v1/rest/__pycache__/options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0577f8d1265fde4beba0d1024f5c002984bc37fc Binary files /dev/null and b/deepgram/clients/listen/v1/rest/__pycache__/options.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/rest/__pycache__/response.cpython-313.pyc b/deepgram/clients/listen/v1/rest/__pycache__/response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e230f62d9eb9eb5ed2cbef5ee2dfbd9ba0ce3ccc Binary files /dev/null and b/deepgram/clients/listen/v1/rest/__pycache__/response.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/rest/async_client.py b/deepgram/clients/listen/v1/rest/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..ef9360a41e309bed5cf898e3d1be84e2c0f20fdc --- /dev/null +++ b/deepgram/clients/listen/v1/rest/async_client.py @@ -0,0 +1,347 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Dict, Union, Optional + +import httpx + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ....common import AbstractAsyncRestClient +from ....common import DeepgramError, DeepgramTypeError + +from .helpers import is_buffer_source, is_readstream_source, is_url_source +from .options import ( + ListenRESTOptions, + PrerecordedOptions, + FileSource, + UrlSource, +) +from .response import AsyncPrerecordedResponse, PrerecordedResponse + + +class AsyncListenRESTClient(AbstractAsyncRestClient): + """ + A client class for handling pre-recorded audio data. + Provides methods for transcribing audio from URLs and files. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + super().__init__(config) + + # pylint: disable=too-many-positional-arguments + + async def transcribe_url( + self, + source: UrlSource, + options: Optional[Union[Dict, ListenRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + **kwargs, + ) -> Union[AsyncPrerecordedResponse, PrerecordedResponse]: + """ + Transcribes audio from a URL source. + + Args: + source (UrlSource): The URL source of the audio to transcribe. + options (ListenRESTOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + PrerecordedResponse: An object containing the transcription result. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_url ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, ListenRESTOptions) and options.callback is not None): + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + return await self.transcribe_url_callback( + source, + callback=options["callback"], + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + url = f"{self._config.url}/{endpoint}" + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, ListenRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, ListenRESTOptions): + self._logger.info("ListenRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = PrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_url succeeded") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + return res + + async def transcribe_url_callback( + self, + source: UrlSource, + callback: str, + options: Optional[Union[Dict, ListenRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + **kwargs, + ) -> AsyncPrerecordedResponse: + """ + Transcribes audio from a URL source and sends the result to a callback URL. + + Args: + source (UrlSource): The URL source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (ListenRESTOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + AsyncPrerecordedResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_url_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, ListenRESTOptions): + options.callback = callback + else: + options["callback"] = callback + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, ListenRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, ListenRESTOptions): + self._logger.info("ListenRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AsyncPrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_url_callback succeeded") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") + return res + + async def transcribe_file( + self, + source: FileSource, + options: Optional[Union[Dict, ListenRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + **kwargs, + ) -> Union[AsyncPrerecordedResponse, PrerecordedResponse]: + """ + Transcribes audio from a local file source. + + Args: + source (FileSource): The local file source of the audio to transcribe. + options (ListenRESTOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + PrerecordedResponse: An object containing the transcription result or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_file ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, ListenRESTOptions) and options.callback is not None): + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + return await self.transcribe_file_callback( + source, + callback=options["callback"], + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + url = f"{self._config.url}/{endpoint}" + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, ListenRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, ListenRESTOptions): + self._logger.info("ListenRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, + options=options, + addons=addons, + headers=headers, + content=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = PrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_file succeeded") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + return res + + async def transcribe_file_callback( + self, + source: FileSource, + callback: str, + options: Optional[Union[Dict, ListenRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + **kwargs, + ) -> AsyncPrerecordedResponse: + """ + Transcribes audio from a local file source and sends the result to a callback URL. + + Args: + source (FileSource): The local file source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (ListenRESTOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + AsyncPrerecordedResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_file_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, ListenRESTOptions): + options.callback = callback + else: + options["callback"] = callback + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, ListenRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, ListenRESTOptions): + self._logger.info("ListenRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, + options=options, + addons=addons, + headers=headers, + content=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AsyncPrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_file_callback succeeded") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") + return res + + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/listen/v1/rest/client.py b/deepgram/clients/listen/v1/rest/client.py new file mode 100644 index 0000000000000000000000000000000000000000..3accab56c83062a51ee926e92155b399f2a9dd20 --- /dev/null +++ b/deepgram/clients/listen/v1/rest/client.py @@ -0,0 +1,348 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Dict, Union, Optional + +import httpx + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ....common import AbstractSyncRestClient +from ....common import DeepgramError, DeepgramTypeError + +from .helpers import is_buffer_source, is_readstream_source, is_url_source +from .options import ( + ListenRESTOptions, + PrerecordedOptions, + FileSource, + UrlSource, +) +from .response import AsyncPrerecordedResponse, PrerecordedResponse + + +class ListenRESTClient(AbstractSyncRestClient): + """ + A client class for handling pre-recorded audio data. + Provides methods for transcribing audio from URLs and files. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + super().__init__(config) + + # pylint: disable=too-many-positional-arguments + + def transcribe_url( + self, + source: UrlSource, + options: Optional[Union[Dict, ListenRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + **kwargs, + ) -> Union[AsyncPrerecordedResponse, PrerecordedResponse]: + """ + Transcribes audio from a URL source. + + Args: + source (UrlSource): The URL source of the audio to transcribe. + options (ListenRESTOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + PrerecordedResponse: An object containing the transcription result. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_url ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, ListenRESTOptions) and options.callback is not None): + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + return self.transcribe_url_callback( + source, + callback=options["callback"], + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + url = f"{self._config.url}/{endpoint}" + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, ListenRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, ListenRESTOptions): + self._logger.info("ListenRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = PrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_url succeeded") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + return res + + def transcribe_url_callback( + self, + source: UrlSource, + callback: str, + options: Optional[Union[Dict, ListenRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + **kwargs, + ) -> AsyncPrerecordedResponse: + """ + Transcribes audio from a URL source and sends the result to a callback URL. + + Args: + source (UrlSource): The URL source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (ListenRESTOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + AsyncPrerecordedResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_url_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, ListenRESTOptions): + options.callback = callback + else: + options["callback"] = callback + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, ListenRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, ListenRESTOptions): + self._logger.info("ListenRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AsyncPrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_url_callback succeeded") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") + return res + + def transcribe_file( + self, + source: FileSource, + options: Optional[Union[Dict, ListenRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + **kwargs, + ) -> Union[AsyncPrerecordedResponse, PrerecordedResponse]: + """ + Transcribes audio from a local file source. + + Args: + source (FileSource): The local file source of the audio to transcribe. + options (ListenRESTOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + PrerecordedResponse: An object containing the transcription result or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_file ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, ListenRESTOptions) and options.callback is not None): + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + return self.transcribe_file_callback( + source, + callback=options["callback"], + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + url = f"{self._config.url}/{endpoint}" + + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, ListenRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, ListenRESTOptions): + self._logger.info("ListenRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + content=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = PrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_file succeeded") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + return res + + def transcribe_file_callback( + self, + source: FileSource, + callback: str, + options: Optional[Union[Dict, ListenRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + **kwargs, + ) -> AsyncPrerecordedResponse: + """ + Transcribes audio from a local file source and sends the result to a callback URL. + + Args: + source (FileSource): The local file source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (ListenRESTOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + AsyncPrerecordedResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_file_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, ListenRESTOptions): + options.callback = callback + else: + options["callback"] = callback + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, ListenRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, ListenRESTOptions): + self._logger.info("ListenRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + content=body, + timeout=timeout, + **kwargs, + ) + self._logger.info("json: %s", result) + res = AsyncPrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_file_callback succeeded") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") + return res + + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/listen/v1/rest/helpers.py b/deepgram/clients/listen/v1/rest/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..2def898f3a7c65d6c1d4caf97f769ef54c2932de --- /dev/null +++ b/deepgram/clients/listen/v1/rest/helpers.py @@ -0,0 +1,26 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .options import PrerecordedSource + + +def is_buffer_source(provided_source: PrerecordedSource) -> bool: + """ + Check if the provided source is a buffer source. + """ + return "buffer" in provided_source + + +def is_readstream_source(provided_source: PrerecordedSource) -> bool: + """ + Check if the provided source is a readstream source. + """ + return "stream" in provided_source + + +def is_url_source(provided_source: PrerecordedSource) -> bool: + """ + Check if the provided source is a URL source. + """ + return "url" in provided_source diff --git a/deepgram/clients/listen/v1/rest/options.py b/deepgram/clients/listen/v1/rest/options.py new file mode 100644 index 0000000000000000000000000000000000000000..1c44d54e4662ef6023ac7b2ceda59d0dc30f18bf --- /dev/null +++ b/deepgram/clients/listen/v1/rest/options.py @@ -0,0 +1,181 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import Union, List, Optional +import logging + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from deepgram.utils import verboselogs +from ....common import ( + TextSource, + StreamSource, + BufferSource, + FileSource, + UrlSource, + BaseResponse, +) + + +@dataclass +class PrerecordedOptions(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + Contains all the options for the PrerecordedClient. + + Reference: + https://developers.deepgram.com/reference/pre-recorded + """ + + alternatives: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + channels: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + callback: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + callback_method: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + custom_intent: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + custom_intent_mode: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + custom_topic: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + custom_topic_mode: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + detect_entities: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + detect_language: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + detect_topics: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + diarize: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + diarize_version: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + dictation: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + encoding: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + extra: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + filler_words: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + intents: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + keyterm: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + keywords: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + language: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + measurements: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + model: Optional[str] = field( + default="None", metadata=dataclass_config(exclude=lambda f: f is None) + ) + multichannel: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + numerals: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + paragraphs: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + profanity_filter: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + punctuate: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + redact: Optional[Union[List[str], bool, str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + replace: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sample_rate: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + search: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + smart_format: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + summarize: Optional[Union[bool, str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + tag: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + tier: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + topics: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + utt_split: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + utterances: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + version: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def check(self): + """ + Check the options for any deprecated fields or values. + """ + logger = verboselogs.VerboseLogger(__name__) + logger.addHandler(logging.StreamHandler()) + prev = logger.level + logger.setLevel(verboselogs.ERROR) + + if self.tier: + logger.error( + "WARNING: Tier is deprecated. Will be removed in a future version." + ) + + logger.setLevel(prev) + + return True + + +ListenRESTOptions = PrerecordedOptions + +# unique +PrerecordedSource = Union[UrlSource, FileSource] +ListenRestSource = PrerecordedSource + +# PrerecordedSource for backwards compatibility +PreRecordedStreamSource = StreamSource diff --git a/deepgram/clients/listen/v1/rest/response.py b/deepgram/clients/listen/v1/rest/response.py new file mode 100644 index 0000000000000000000000000000000000000000..e24e9c2fc0e5840efbef8528b3a58ee5707b831d --- /dev/null +++ b/deepgram/clients/listen/v1/rest/response.py @@ -0,0 +1,470 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Dict, Any + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +# between analyze and listen +from ....common import ( + BaseResponse, + Average, + Intent, + Intents, + IntentsInfo, + Segment, + SentimentInfo, + Sentiment, + Sentiments, + SummaryInfo, + Topic, + Topics, + TopicsInfo, +) + +# between rest and websocket +from ....common import ( + ModelInfo, + Hit, + Search, +) + +# Async Prerecorded Response Types: + + +@dataclass +class AsyncPrerecordedResponse(BaseResponse): + """ + The response object for the async prerecorded API. + """ + + request_id: str = "" + + +# Prerecorded Response Types: + + +@dataclass +class Metadata(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + The metadata for the response. + """ + + transaction_key: str = "" + request_id: str = "" + sha256: str = "" + created: str = "" + duration: float = 0 + channels: int = 0 + models: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + # pylint: disable=used-before-assignment + warnings: Optional[List[Warning]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + # pylint: enable=used-before-assignment + model_info: Optional[Dict[str, ModelInfo]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + summary_info: Optional[SummaryInfo] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + intents_info: Optional[IntentsInfo] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment_info: Optional[SentimentInfo] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + topics_info: Optional[TopicsInfo] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + extra: Optional[Dict] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "models" in _dict: + _dict["models"] = [str(models) for models in _dict["models"]] + if "warnings" in _dict: + _dict["warnings"] = [ + Warning.from_dict(warnings) for warnings in _dict["warnings"] + ] + if "model_info" in _dict: + _dict["model_info"] = [ + ModelInfo.from_dict(model_info) + for _, model_info in _dict["model_info"].items() + ] + if "summary_info" in _dict: + _dict["summary_info"] = SummaryInfo.from_dict(_dict["summary_info"]) + if "intents_info" in _dict: + _dict["intents_info"] = IntentsInfo.from_dict(_dict["intents_info"]) + if "sentiment_info" in _dict: + _dict["sentiment_info"] = SentimentInfo.from_dict(_dict["sentiment_info"]) + if "topics_info" in _dict: + _dict["topics_info"] = TopicsInfo.from_dict(_dict["topics_info"]) + if "extra" in _dict: + _dict["extra"] = [str(extra) for _, extra in _dict["extra"].items()] + return _dict[key] + + +@dataclass +class SummaryV1(BaseResponse): + """ + The summary information for the response. + """ + + summary: str = "" + start_word: float = 0 + end_word: float = 0 + + +Summaries = SummaryV1 + + +@dataclass +class SummaryV2(BaseResponse): + """ + The summary information for the response. + """ + + result: str = "" + short: str = "" + + +Summary = SummaryV2 + + +@dataclass +class ListenRESTWord(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + The word information for the response. + """ + + word: str = "" + start: float = 0 + end: float = 0 + confidence: float = 0 + punctuated_word: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + speaker: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + language: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + speaker_confidence: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment: Optional[Sentiment] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment_score: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "sentiment" in _dict: + _dict["sentiment"] = Sentiment.from_dict(_dict["sentiment"]) + return _dict[key] + + +@dataclass +class Sentence(BaseResponse): + """ + The sentence information for the response. + """ + + text: str = "" + start: float = 0 + end: float = 0 + sentiment: Optional[Sentiment] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment_score: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "sentiment" in _dict: + _dict["sentiment"] = Sentiment.from_dict(_dict["sentiment"]) + return _dict[key] + + +@dataclass +class Paragraph(BaseResponse): + """ + The paragraph information for the response. + """ + + sentences: List[Sentence] = field(default_factory=list) + start: float = 0 + end: float = 0 + num_words: int = 0 + speaker: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment: Optional[Sentiment] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment_score: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "sentences" in _dict: + _dict["sentences"] = [ + Sentence.from_dict(sentences) for sentences in _dict["sentences"] + ] + if "sentiment" in _dict: + _dict["sentiment"] = Sentiment.from_dict(_dict["sentiment"]) + return _dict[key] + + +@dataclass +class Paragraphs(BaseResponse): + """ + The paragraphs information for the response. + """ + + transcript: Optional[str] = "" + paragraphs: Optional[List[Paragraph]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "paragraphs" in _dict: + _dict["paragraphs"] = [ + Paragraph.from_dict(paragraphs) for paragraphs in _dict["paragraphs"] + ] + return _dict[key] + + +@dataclass +class Translation(BaseResponse): + """ + The translation information for the response. + """ + + language: Optional[str] = "" + translation: Optional[str] = "" + + +@dataclass +class Warning(BaseResponse): # pylint: disable=used-before-assignment,redefined-builtin + """ + The warning information for the response. + """ + + parameter: str = "" + type: str = "" + message: str = "" + + +@dataclass +class Utterance(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + The utterance information for the response. + """ + + start: float = 0 + end: float = 0 + confidence: float = 0 + channel: int = 0 + transcript: str = "" + words: List[ListenRESTWord] = field(default_factory=list) + speaker: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment: Optional[Sentiment] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment_score: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + id: str = "" + + def __getitem__(self, key): + _dict = self.to_dict() + if "words" in _dict: + _dict["words"] = [ + ListenRESTWord.from_dict(words) for words in _dict["words"] + ] + if "sentiment" in _dict: + _dict["sentiment"] = Sentiment.from_dict(_dict["sentiment"]) + return _dict[key] + + +@dataclass +class Entity(BaseResponse): + """ + The entity information for the response. + """ + + label: str = "" + value: str = "" + confidence: float = 0 + start_word: float = 0 + end_word: float = 0 + + +@dataclass +class ListenRESTAlternative( + BaseResponse +): # pylint: disable=too-many-instance-attributes + """ + The alternative information for the response. + """ + + transcript: str = "" + confidence: float = 0 + words: List[ListenRESTWord] = field(default_factory=list) + summaries: Optional[List[SummaryV1]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + paragraphs: Optional[Paragraphs] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + entities: Optional[List[Entity]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + translations: Optional[List[Translation]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + languages: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "words" in _dict: + _dict["words"] = [ + ListenRESTWord.from_dict(words) for words in _dict["words"] + ] + if "summaries" in _dict: + _dict["summaries"] = [ + SummaryV1.from_dict(summaries) for summaries in _dict["summaries"] + ] + if "paragraphs" in _dict: + _dict["paragraphs"] = Paragraphs.from_dict(_dict["paragraphs"]) + if "entities" in _dict: + _dict["entities"] = [ + Entity.from_dict(entities) for entities in _dict["entities"] + ] + if "translations" in _dict: + _dict["translations"] = [ + Translation.from_dict(translations) + for translations in _dict["translations"] + ] + return _dict[key] + + +@dataclass +class ListenRESTChannel(BaseResponse): + """ + The channel information for the response. + """ + + search: Optional[List[Search]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + alternatives: List[ListenRESTAlternative] = field(default_factory=list) + detected_language: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + language_confidence: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "search" in _dict: + _dict["search"] = [Search.from_dict(search) for search in _dict["search"]] + if "alternatives" in _dict: + _dict["alternatives"] = [ + ListenRESTAlternative.from_dict(alternatives) + for alternatives in _dict["alternatives"] + ] + return _dict[key] + + +@dataclass +class Results(BaseResponse): + """ + The results information for the response. + """ + + channels: Optional[List[ListenRESTChannel]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + utterances: Optional[List[Utterance]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + summary: Optional[SummaryV2] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiments: Optional[Sentiments] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + topics: Optional[Topics] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + intents: Optional[Intents] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "channels" in _dict: + _dict["channels"] = [ + ListenRESTChannel.from_dict(channels) for channels in _dict["channels"] + ] + if "utterances" in _dict: + _dict["utterances"] = [ + Utterance.from_dict(utterances) for utterances in _dict["utterances"] + ] + if "summary" in _dict: + _dict["summary"] = SummaryV2.from_dict(_dict["summary"]) + if "sentiments" in _dict: + _dict["sentiments"] = Sentiments.from_dict(_dict["sentiments"]) + if "topics" in _dict: + _dict["topics"] = Topics.from_dict(_dict["topics"]) + if "intents" in _dict: + _dict["intents"] = Intents.from_dict(_dict["intents"]) + return _dict[key] + + +# Prerecorded Response Result: + + +@dataclass +class PrerecordedResponse(BaseResponse): + """ + The response object for the prerecorded API. + """ + + metadata: Optional[Metadata] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + results: Optional[Results] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "metadata" in _dict: + _dict["metadata"] = Metadata.from_dict(_dict["metadata"]) + if "results" in _dict: + _dict["results"] = Results.from_dict(_dict["results"]) + return _dict[key] + + +SyncPrerecordedResponse = PrerecordedResponse diff --git a/deepgram/clients/listen/v1/websocket/__init__.py b/deepgram/clients/listen/v1/websocket/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9f6891d899547c60141d9c1b1399780498a2fe4b --- /dev/null +++ b/deepgram/clients/listen/v1/websocket/__init__.py @@ -0,0 +1,31 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import ListenWebSocketClient +from .async_client import AsyncListenWebSocketClient +from .options import LiveOptions, ListenWebSocketOptions + +# unique websocket response +from .response import ( + #### top level + LiveResultResponse, + MetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + #### common websocket response + BaseResponse, + OpenResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, + #### between rest and websocket + ModelInfo, + Hit, + Search, + #### unique + Metadata, + ListenWSWord, + ListenWSAlternative, + ListenWSChannel, +) diff --git a/deepgram/clients/listen/v1/websocket/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/listen/v1/websocket/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..836b182edc9ff9de45977292ee81f3ba6ae4d4d6 Binary files /dev/null and b/deepgram/clients/listen/v1/websocket/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/websocket/__pycache__/async_client.cpython-313.pyc b/deepgram/clients/listen/v1/websocket/__pycache__/async_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df6d10590e341a0877d2ef94509b41860b2a28b7 Binary files /dev/null and b/deepgram/clients/listen/v1/websocket/__pycache__/async_client.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/websocket/__pycache__/client.cpython-313.pyc b/deepgram/clients/listen/v1/websocket/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba58753f30320af8fe340fb2cf8315ccc6ce8095 Binary files /dev/null and b/deepgram/clients/listen/v1/websocket/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/websocket/__pycache__/options.cpython-313.pyc b/deepgram/clients/listen/v1/websocket/__pycache__/options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cec332221ec9301398b501ea09129765a9b37efe Binary files /dev/null and b/deepgram/clients/listen/v1/websocket/__pycache__/options.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/websocket/__pycache__/response.cpython-313.pyc b/deepgram/clients/listen/v1/websocket/__pycache__/response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3bc7d60ed6f4ae739e4a7dc2200863eb3b8f497e Binary files /dev/null and b/deepgram/clients/listen/v1/websocket/__pycache__/response.cpython-313.pyc differ diff --git a/deepgram/clients/listen/v1/websocket/async_client.py b/deepgram/clients/listen/v1/websocket/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..0ed81c0c30d6bf607505c0008dd568da5095fb7c --- /dev/null +++ b/deepgram/clients/listen/v1/websocket/async_client.py @@ -0,0 +1,587 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT +import asyncio +import json +import logging +from typing import Dict, Union, Optional, cast, Any, Callable +from datetime import datetime +import threading + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import LiveTranscriptionEvents +from ....common import AbstractAsyncWebSocketClient +from ....common import DeepgramError + +from .response import ( + OpenResponse, + LiveResultResponse, + MetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) +from .options import ListenWebSocketOptions + +ONE_SECOND = 1 +HALF_SECOND = 0.5 +DEEPGRAM_INTERVAL = 5 +PING_INTERVAL = 20 + + +class AsyncListenWebSocketClient( + AbstractAsyncWebSocketClient +): # pylint: disable=too-many-instance-attributes + """ + Client for interacting with Deepgram's live transcription services over WebSockets. + + This class provides methods to establish a WebSocket connection for live transcription and handle real-time transcription events. + + Args: + config (DeepgramClientOptions): all the options for the client. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + _event_handlers: Dict[LiveTranscriptionEvents, list] + + _keep_alive_thread: Union[asyncio.Task, None] + _flush_thread: Union[asyncio.Task, None] + _last_datagram: Optional[datetime] = None + + _kwargs: Optional[Dict] = None + _addons: Optional[Dict] = None + _options: Optional[Dict] = None + _headers: Optional[Dict] = None + + def __init__(self, config: DeepgramClientOptions): + if config is None: + raise DeepgramError("Config is required") + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + self._endpoint = "v1/listen" + + self._flush_thread = None + self._keep_alive_thread = None + + # auto flush + self._last_datagram = None + self._lock_flush = threading.Lock() + + # init handlers + self._event_handlers = { + event: [] for event in LiveTranscriptionEvents.__members__.values() + } + + # call the parent constructor + super().__init__(self._config, self._endpoint) + + # pylint: disable=too-many-branches,too-many-statements + async def start( + self, + options: Optional[Union[ListenWebSocketOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + members: Optional[Dict] = None, + **kwargs, + ) -> bool: + """ + Starts the WebSocket connection for live transcription. + """ + self._logger.debug("AsyncListenWebSocketClient.start ENTER") + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + self._logger.info("members: %s", members) + self._logger.info("kwargs: %s", kwargs) + + if isinstance(options, ListenWebSocketOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AsyncListenWebSocketClient.start LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._addons = addons + self._headers = headers + + # add "members" as members of the class + if members is not None: + self.__dict__.update(members) + + # set kwargs as members of the class + if kwargs is not None: + self._kwargs = kwargs + else: + self._kwargs = {} + + if isinstance(options, ListenWebSocketOptions): + self._logger.info("ListenWebSocketOptions switching class -> dict") + self._options = options.to_dict() + elif options is not None: + self._options = options + else: + self._options = {} + + try: + # call parent start + if ( + await super().start( + self._options, + self._addons, + self._headers, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + is False + ): + self._logger.error("AsyncListenWebSocketClient.start failed") + self._logger.debug("AsyncListenWebSocketClient.start LEAVE") + return False + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # keepalive thread + if self._config.is_keep_alive_enabled(): + self._logger.notice("keepalive is enabled") + self._keep_alive_thread = asyncio.create_task(self._keep_alive()) + else: + self._logger.notice("keepalive is disabled") + + # flush thread + if self._config.is_auto_flush_reply_enabled(): + self._logger.notice("autoflush is enabled") + self._flush_thread = asyncio.create_task(self._flush()) + else: + self._logger.notice("autoflush is disabled") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("start succeeded") + self._logger.debug("AsyncListenWebSocketClient.start LEAVE") + return True + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "WebSocketException in AsyncListenWebSocketClient.start: %s", e + ) + self._logger.debug("AsyncListenWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect") is True: + raise + return False + + # pylint: enable=too-many-branches,too-many-statements + + def on(self, event: LiveTranscriptionEvents, handler: Callable) -> None: + """ + Registers event handlers for specific events. + """ + self._logger.info("event subscribed: %s", event) + if event in LiveTranscriptionEvents.__members__.values() and callable(handler): + self._event_handlers[event].append(handler) + + # triggers the registered event handlers for a specific event + async def _emit(self, event: LiveTranscriptionEvents, *args, **kwargs) -> None: + """ + Emits events to the registered event handlers. + """ + self._logger.debug("AsyncListenWebSocketClient._emit ENTER") + self._logger.debug("callback handlers for: %s", event) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + tasks = [] + for handler in self._event_handlers[event]: + task = asyncio.create_task(handler(self, *args, **kwargs)) + tasks.append(task) + + if tasks: + self._logger.debug("waiting for tasks to finish...") + await asyncio.gather(*tasks, return_exceptions=True) + tasks.clear() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("AsyncListenWebSocketClient._emit LEAVE") + + async def _process_text(self, message: str) -> None: + """ + Processes messages received over the WebSocket connection. + """ + self._logger.debug("AsyncListenWebSocketClient._process_text ENTER") + + try: + self._logger.debug("Text data received") + if len(message) == 0: + self._logger.debug("message is empty") + self._logger.debug("AsyncListenWebSocketClient._process_text LEAVE") + return + + data = json.loads(message) + response_type = data.get("type") + self._logger.debug("response_type: %s, data: %s", response_type, data) + + match response_type: + case LiveTranscriptionEvents.Open: + open_result: OpenResponse = OpenResponse.from_json(message) + self._logger.verbose("OpenResponse: %s", open_result) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Open), + open=open_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Transcript: + msg_result: LiveResultResponse = LiveResultResponse.from_json( + message + ) + self._logger.verbose("LiveResultResponse: %s", msg_result) + + # auto flush + if self._config.is_inspecting_listen(): + inspect_res = await self._inspect(msg_result) + if not inspect_res: + self._logger.error("inspect_res failed") + + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Transcript), + result=msg_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Metadata: + meta_result: MetadataResponse = MetadataResponse.from_json(message) + self._logger.verbose("MetadataResponse: %s", meta_result) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Metadata), + metadata=meta_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.SpeechStarted: + ss_result: SpeechStartedResponse = SpeechStartedResponse.from_json( + message + ) + self._logger.verbose("SpeechStartedResponse: %s", ss_result) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.SpeechStarted), + speech_started=ss_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.UtteranceEnd: + ue_result: UtteranceEndResponse = UtteranceEndResponse.from_json( + message + ) + self._logger.verbose("UtteranceEndResponse: %s", ue_result) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.UtteranceEnd), + utterance_end=ue_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Close: + close_result: CloseResponse = CloseResponse.from_json(message) + self._logger.verbose("CloseResponse: %s", close_result) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Close), + close=close_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Error: + err_error: ErrorResponse = ErrorResponse.from_json(message) + self._logger.verbose("ErrorResponse: %s", err_error) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case _: + self._logger.warning( + "Unknown Message: response_type: %s, data: %s", + response_type, + data, + ) + unhandled_error: UnhandledResponse = UnhandledResponse( + type=LiveTranscriptionEvents(LiveTranscriptionEvents.Unhandled), + raw=message, + ) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Unhandled), + unhandled=unhandled_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_text Succeeded") + self._logger.debug("AsyncListenWebSocketClient._process_text LEAVE") + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AsyncListenWebSocketClient._process_text: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AsyncListenWebSocketClient._process_text", + f"{e}", + "Exception", + ) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await super()._signal_exit() + + self._logger.debug("AsyncListenWebSocketClient._process_text LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-return-statements,too-many-statements + + async def _process_binary(self, message: bytes) -> None: + raise NotImplementedError("no _process_binary method should be called") + + # pylint: disable=too-many-return-statements + async def _keep_alive(self) -> None: + """ + Sends keepalive messages to the WebSocket connection. + """ + self._logger.debug("AsyncListenWebSocketClient._keep_alive ENTER") + + counter = 0 + while True: + try: + counter += 1 + await asyncio.sleep(ONE_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_keep_alive exiting gracefully") + self._logger.debug("AsyncListenWebSocketClient._keep_alive LEAVE") + return + + # deepgram keepalive + if counter % DEEPGRAM_INTERVAL == 0: + await self.keep_alive() + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AsyncListenWebSocketClient._keep_alive: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AsyncListenWebSocketClient._keep_alive", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in AsyncListenWebSocketClient._keep_alive: %s", str(e) + ) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await super()._signal_exit() + + self._logger.debug("AsyncListenWebSocketClient._keep_alive LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + ## pylint: disable=too-many-return-statements,too-many-statements + async def _flush(self) -> None: + self._logger.debug("AsyncListenWebSocketClient._flush ENTER") + + delta_in_ms_str = self._config.options.get("auto_flush_reply_delta") + if delta_in_ms_str is None: + self._logger.error("auto_flush_reply_delta is None") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") + return + delta_in_ms = float(delta_in_ms_str) + + while True: + try: + await asyncio.sleep(HALF_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_flush exiting gracefully") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") + return + + if self._last_datagram is None: + self._logger.debug("AutoFlush last_datagram is None") + continue + + delta = datetime.now() - self._last_datagram + diff_in_ms = delta.total_seconds() * 1000 + self._logger.debug("AutoFlush delta: %f", diff_in_ms) + if diff_in_ms < delta_in_ms: + self._logger.debug("AutoFlush delta is less than threshold") + continue + + self._last_datagram = None + await self.finalize() + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AsyncListenWebSocketClient._flush: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AsyncListenWebSocketClient._flush", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in AsyncListenWebSocketClient._flush: %s", str(e) + ) + await self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await super()._signal_exit() + + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-return-statements + + async def keep_alive(self) -> bool: + """ + Sends a KeepAlive message + """ + self._logger.spam("AsyncListenWebSocketClient.keep_alive ENTER") + + self._logger.notice("Sending KeepAlive...") + ret = await self.send(json.dumps({"type": "KeepAlive"})) + + if not ret: + self._logger.error("keep_alive failed") + self._logger.spam("AsyncListenWebSocketClient.keep_alive LEAVE") + return False + + self._logger.notice("keep_alive succeeded") + self._logger.spam("AsyncListenWebSocketClient.keep_alive LEAVE") + + return True + + async def finalize(self) -> bool: + """ + Finalizes the Transcript connection by flushing it + """ + self._logger.spam("AsyncListenWebSocketClient.finalize ENTER") + + self._logger.notice("Sending Finalize...") + ret = await self.send(json.dumps({"type": "Finalize"})) + + if not ret: + self._logger.error("finalize failed") + self._logger.spam("AsyncListenWebSocketClient.finalize LEAVE") + return False + + self._logger.notice("finalize succeeded") + self._logger.spam("AsyncListenWebSocketClient.finalize LEAVE") + + return True + + async def _close_message(self) -> bool: + return await self.send(json.dumps({"type": "CloseStream"})) + + async def finish(self) -> bool: + """ + Closes the WebSocket connection gracefully. + """ + self._logger.debug("AsyncListenWebSocketClient.finish ENTER") + + # stop the threads + self._logger.verbose("cancelling tasks...") + try: + # call parent finish + if await super().finish() is False: + self._logger.error("AsyncListenWebSocketClient.finish failed") + + # Before cancelling, check if the tasks were created + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + tasks = [] + if self._keep_alive_thread is not None: + self._keep_alive_thread.cancel() + tasks.append(self._keep_alive_thread) + self._logger.notice("processing _keep_alive_thread cancel...") + + if self._flush_thread is not None: + self._flush_thread.cancel() + tasks.append(self._flush_thread) + self._logger.notice("processing _flush_thread cancel...") + + # Use asyncio.gather to wait for tasks to be cancelled + # Prevent indefinite waiting by setting a timeout + await asyncio.wait_for(asyncio.gather(*tasks), timeout=10) + self._logger.notice("threads joined") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("finish succeeded") + self._logger.spam("AsyncListenWebSocketClient.finish LEAVE") + return True + + except asyncio.CancelledError as e: + self._logger.error("tasks cancelled error: %s", e) + self._logger.debug("AsyncListenWebSocketClient.finish LEAVE") + return False + + except asyncio.TimeoutError as e: + self._logger.error("tasks cancellation timed out: %s", e) + self._logger.debug("AsyncListenWebSocketClient.finish LEAVE") + return False + + async def _inspect(self, msg_result: LiveResultResponse) -> bool: + # auto flush_inspect is generically used to track any messages you might want to snoop on + # place additional logic here to inspect messages of interest + + # for auto flush functionality + # set the last datagram + sentence = msg_result.channel.alternatives[0].transcript + if len(sentence) == 0: + return True + + if msg_result.is_final: + self._logger.debug("AutoFlush is_final received") + self._last_datagram = None + else: + self._last_datagram = datetime.now() + self._logger.debug( + "AutoFlush interim received: %s", + str(self._last_datagram), + ) + + return True diff --git a/deepgram/clients/listen/v1/websocket/client.py b/deepgram/clients/listen/v1/websocket/client.py new file mode 100644 index 0000000000000000000000000000000000000000..e6633689919249987dd3b67ba841a2f06d51f0cd --- /dev/null +++ b/deepgram/clients/listen/v1/websocket/client.py @@ -0,0 +1,590 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT +import json +import time +import logging +from typing import Dict, Union, Optional, cast, Any, Callable, Type +from datetime import datetime +import threading + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import LiveTranscriptionEvents +from ....common import AbstractSyncWebSocketClient +from ....common import DeepgramError + +from .response import ( + OpenResponse, + LiveResultResponse, + MetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) +from .options import ListenWebSocketOptions + +ONE_SECOND = 1 +HALF_SECOND = 0.5 +DEEPGRAM_INTERVAL = 5 +PING_INTERVAL = 20 + + +class ListenWebSocketClient( + AbstractSyncWebSocketClient +): # pylint: disable=too-many-instance-attributes + """ + Client for interacting with Deepgram's live transcription services over WebSockets. + + This class provides methods to establish a WebSocket connection for live transcription and handle real-time transcription events. + + Args: + config (DeepgramClientOptions): all the options for the client. + thread_cls (Type[threading.Thread]): optional thread class to use for creating threads, + defaults to threading.Thread. Useful for custom thread management like ContextVar support. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + _lock_flush: threading.Lock + _event_handlers: Dict[LiveTranscriptionEvents, list] + + _keep_alive_thread: Union[threading.Thread, None] + _flush_thread: Union[threading.Thread, None] + _last_datagram: Optional[datetime] = None + + _thread_cls: Type[threading.Thread] + + _kwargs: Optional[Dict] = None + _addons: Optional[Dict] = None + _options: Optional[Dict] = None + _headers: Optional[Dict] = None + + def __init__( + self, + config: DeepgramClientOptions, + thread_cls: Type[threading.Thread] = threading.Thread, + ): + if config is None: + raise DeepgramError("Config is required") + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + self._endpoint = "v1/listen" + + self._flush_thread = None + self._keep_alive_thread = None + + # auto flush + self._last_datagram = None + self._lock_flush = threading.Lock() + + self._thread_cls = thread_cls + + # init handlers + self._event_handlers = { + event: [] for event in LiveTranscriptionEvents.__members__.values() + } + + # call the parent constructor + super().__init__( + config=self._config, + endpoint=self._endpoint, + thread_cls=self._thread_cls, + ) + + # pylint: disable=too-many-statements,too-many-branches + def start( + self, + options: Optional[Union[ListenWebSocketOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + members: Optional[Dict] = None, + **kwargs, + ) -> bool: + """ + Starts the WebSocket connection for live transcription. + """ + self._logger.debug("ListenWebSocketClient.start ENTER") + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + self._logger.info("members: %s", members) + self._logger.info("kwargs: %s", kwargs) + + if isinstance(options, ListenWebSocketOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenWebSocketClient.start LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._addons = addons + self._headers = headers + + # add "members" as members of the class + if members is not None: + self.__dict__.update(members) + + # set kwargs as members of the class + if kwargs is not None: + self._kwargs = kwargs + else: + self._kwargs = {} + + if isinstance(options, ListenWebSocketOptions): + self._logger.info("ListenWebSocketOptions switching class -> dict") + self._options = options.to_dict() + elif options is not None: + self._options = options + else: + self._options = {} + + try: + # call parent start + if ( + super().start( + self._options, + self._addons, + self._headers, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + is False + ): + self._logger.error("ListenWebSocketClient.start failed") + self._logger.debug("ListenWebSocketClient.start LEAVE") + return False + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # keepalive thread + if self._config.is_keep_alive_enabled(): + self._logger.notice("keepalive is enabled") + self._keep_alive_thread = self._thread_cls(target=self._keep_alive) + self._keep_alive_thread.start() + else: + self._logger.notice("keepalive is disabled") + + # flush thread + if self._config.is_auto_flush_reply_enabled(): + self._logger.notice("autoflush is enabled") + self._flush_thread = self._thread_cls(target=self._flush) + self._flush_thread.start() + else: + self._logger.notice("autoflush is disabled") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("start succeeded") + self._logger.debug("ListenWebSocketClient.start LEAVE") + return True + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "WebSocketException in ListenWebSocketClient.start: %s", e + ) + self._logger.debug("ListenWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect") is True: + raise e + return False + + # pylint: enable=too-many-statements,too-many-branches + + def on( + self, event: LiveTranscriptionEvents, handler: Callable + ) -> None: # registers event handlers for specific events + """ + Registers event handlers for specific events. + """ + self._logger.info("event subscribed: %s", event) + if event in LiveTranscriptionEvents.__members__.values() and callable(handler): + self._event_handlers[event].append(handler) + + def _emit(self, event: LiveTranscriptionEvents, *args, **kwargs) -> None: + """ + Emits events to the registered event handlers. + """ + self._logger.debug("ListenWebSocketClient._emit ENTER") + self._logger.debug("callback handlers for: %s", event) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("callback handlers for: %s", event) + for handler in self._event_handlers[event]: + handler(self, *args, **kwargs) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("ListenWebSocketClient._emit LEAVE") + + # pylint: disable=too-many-return-statements,too-many-statements,too-many-locals,too-many-branches + def _process_text(self, message: str) -> None: + """ + Processes messages received over the WebSocket connection. + """ + self._logger.debug("ListenWebSocketClient._process_text ENTER") + + try: + if len(message) == 0: + self._logger.debug("message is empty") + self._logger.debug("ListenWebSocketClient._process_text LEAVE") + return + + data = json.loads(message) + response_type = data.get("type") + self._logger.debug("response_type: %s, data: %s", response_type, data) + + match response_type: + case LiveTranscriptionEvents.Open: + open_result: OpenResponse = OpenResponse.from_json(message) + self._logger.verbose("OpenResponse: %s", open_result) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Open), + open=open_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Transcript: + msg_result: LiveResultResponse = LiveResultResponse.from_json( + message + ) + self._logger.verbose("LiveResultResponse: %s", msg_result) + + # auto flush + if self._config.is_inspecting_listen(): + inspect_res = self._inspect(msg_result) + if not inspect_res: + self._logger.error("inspect_res failed") + + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Transcript), + result=msg_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Metadata: + meta_result: MetadataResponse = MetadataResponse.from_json(message) + self._logger.verbose("MetadataResponse: %s", meta_result) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Metadata), + metadata=meta_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.SpeechStarted: + ss_result: SpeechStartedResponse = SpeechStartedResponse.from_json( + message + ) + self._logger.verbose("SpeechStartedResponse: %s", ss_result) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.SpeechStarted), + speech_started=ss_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.UtteranceEnd: + ue_result: UtteranceEndResponse = UtteranceEndResponse.from_json( + message + ) + self._logger.verbose("UtteranceEndResponse: %s", ue_result) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.UtteranceEnd), + utterance_end=ue_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Close: + close_result: CloseResponse = CloseResponse.from_json(message) + self._logger.verbose("CloseResponse: %s", close_result) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Close), + close=close_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Error: + err_error: ErrorResponse = ErrorResponse.from_json(message) + self._logger.verbose("ErrorResponse: %s", err_error) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case _: + self._logger.warning( + "Unknown Message: response_type: %s, data: %s", + response_type, + data, + ) + unhandled_error: UnhandledResponse = UnhandledResponse( + type=LiveTranscriptionEvents(LiveTranscriptionEvents.Unhandled), + raw=message, + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Unhandled), + unhandled=unhandled_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_text Succeeded") + self._logger.debug("SpeakStreamClient._process_text LEAVE") + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in ListenWebSocketClient._process_text: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in ListenWebSocketClient._process_text", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in ListenWebSocketClient._process_text: %s", str(e) + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), + e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + super()._signal_exit() + + self._logger.debug("ListenWebSocketClient._process_text LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-return-statements,too-many-statements + + def _process_binary(self, message: bytes) -> None: + raise NotImplementedError("no _process_binary method should be called") + + # pylint: disable=too-many-return-statements + def _keep_alive(self) -> None: + self._logger.debug("ListenWebSocketClient._keep_alive ENTER") + + counter = 0 + while True: + try: + counter += 1 + self._exit_event.wait(timeout=ONE_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_keep_alive exiting gracefully") + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + return + + # deepgram keepalive + if counter % DEEPGRAM_INTERVAL == 0: + self.keep_alive() + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in ListenWebSocketClient._keep_alive: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in ListenWebSocketClient._keep_alive", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in ListenWebSocketClient._keep_alive: %s", str(e) + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), + e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + super()._signal_exit() + + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + ## pylint: disable=too-many-return-statements,too-many-statements + def _flush(self) -> None: + self._logger.debug("ListenWebSocketClient._flush ENTER") + + delta_in_ms_str = self._config.options.get("auto_flush_reply_delta") + if delta_in_ms_str is None: + self._logger.error("auto_flush_reply_delta is None") + self._logger.debug("ListenWebSocketClient._flush LEAVE") + return + delta_in_ms = float(delta_in_ms_str) + + _flush_event = threading.Event() + while True: + try: + _flush_event.wait(timeout=HALF_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_flush exiting gracefully") + self._logger.debug("ListenWebSocketClient._flush LEAVE") + return + + with self._lock_flush: + if self._last_datagram is None: + self._logger.debug("AutoFlush last_datagram is None") + continue + + delta = datetime.now() - self._last_datagram + diff_in_ms = delta.total_seconds() * 1000 + self._logger.debug("AutoFlush delta: %f", diff_in_ms) + if diff_in_ms < delta_in_ms: + self._logger.debug("AutoFlush delta is less than threshold") + continue + + with self._lock_flush: + self._last_datagram = None + self.finalize() + + except Exception as e: # pylint: disable=broad-except + self._logger.error("Exception in ListenWebSocketClient._flush: %s", e) + e_error: ErrorResponse = ErrorResponse( + "Exception in ListenWebSocketClient._flush", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in ListenWebSocketClient._flush: %s", str(e) + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), + e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + super()._signal_exit() + + self._logger.debug("ListenWebSocketClient._flush LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-return-statements + + def keep_alive(self) -> bool: + """ + Sends a KeepAlive message + """ + self._logger.spam("ListenWebSocketClient.keep_alive ENTER") + + self._logger.notice("Sending KeepAlive...") + ret = self.send(json.dumps({"type": "KeepAlive"})) + + if not ret: + self._logger.error("keep_alive failed") + self._logger.spam("ListenWebSocketClient.keep_alive LEAVE") + return False + + self._logger.notice("keep_alive succeeded") + self._logger.spam("ListenWebSocketClient.keep_alive LEAVE") + + return True + + def finalize(self) -> bool: + """ + Finalizes the Transcript connection by flushing it + """ + self._logger.spam("ListenWebSocketClient.finalize ENTER") + + self._logger.notice("Sending Finalize...") + ret = self.send(json.dumps({"type": "Finalize"})) + + if not ret: + self._logger.error("finalize failed") + self._logger.spam("ListenWebSocketClient.finalize LEAVE") + return False + + self._logger.notice("finalize succeeded") + self._logger.spam("ListenWebSocketClient.finalize LEAVE") + + return True + + def _close_message(self) -> bool: + return self.send(json.dumps({"type": "CloseStream"})) + + # closes the WebSocket connection gracefully + def finish(self) -> bool: + """ + Closes the WebSocket connection gracefully. + """ + self._logger.spam("ListenWebSocketClient.finish ENTER") + + # call parent finish + if super().finish() is False: + self._logger.error("ListenWebSocketClient.finish failed") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # stop the threads + self._logger.verbose("cancelling tasks...") + if self._flush_thread is not None: + self._flush_thread.join() + self._flush_thread = None + self._logger.notice("processing _flush_thread thread joined") + + if self._keep_alive_thread is not None: + self._keep_alive_thread.join() + self._keep_alive_thread = None + self._logger.notice("processing _keep_alive_thread thread joined") + + if self._listen_thread is not None: + self._listen_thread.join() + self._listen_thread = None + self._logger.notice("listening thread joined") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("finish succeeded") + self._logger.spam("ListenWebSocketClient.finish LEAVE") + return True + + def _inspect(self, msg_result: LiveResultResponse) -> bool: + # auto flush_inspect is generically used to track any messages you might want to snoop on + # place additional logic here to inspect messages of interest + + # for auto flush functionality + # set the last datagram + sentence = msg_result.channel.alternatives[0].transcript + if len(sentence) == 0: + return True + + if msg_result.is_final: + with self._lock_flush: + self._logger.debug("AutoFlush is_final received") + self._last_datagram = None + else: + with self._lock_flush: + self._last_datagram = datetime.now() + self._logger.debug( + "AutoFlush interim received: %s", + str(self._last_datagram), + ) + + return True diff --git a/deepgram/clients/listen/v1/websocket/options.py b/deepgram/clients/listen/v1/websocket/options.py new file mode 100644 index 0000000000000000000000000000000000000000..97b141056b41a8a4fcaf804ccaa49b34d0be173e --- /dev/null +++ b/deepgram/clients/listen/v1/websocket/options.py @@ -0,0 +1,153 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Union +import logging + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from deepgram.utils import verboselogs + +from ....common import BaseResponse + + +@dataclass +class LiveOptions(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + Live Transcription Options for the Deepgram Platform. + + Please see the documentation for more information on each option: + https://developers.deepgram.com/reference/streaming + """ + + alternatives: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + callback: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + callback_method: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + channels: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + diarize: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + diarize_version: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + dictation: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + encoding: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + # pylint: disable=W0511 + # TODO: endpointing's current type previous was `Optional[str]` which is incorrect + # for backward compatibility we are keeping it as `Optional[Union[str, bool, int]]` + # since it gets translated to a string to be placed as a query parameter, will keep `str` for now + # but will change this to `Optional[Union[bool, int]]` in a future release + endpointing: Optional[Union[str, bool, int]] = field( + default=None, + metadata=dataclass_config(exclude=lambda f: f is None), + ) + # pylint: enable=W0511 + extra: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + filler_words: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + interim_results: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + keywords: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + keyterm: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + language: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + model: Optional[str] = field( + default="None", metadata=dataclass_config(exclude=lambda f: f is None) + ) + multichannel: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + no_delay: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + numerals: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + punctuate: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + profanity_filter: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + redact: Optional[Union[List[str], bool, str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + replace: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sample_rate: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + search: Optional[Union[List[str], str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + smart_format: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + tag: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + tier: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + utterance_end_ms: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + vad_events: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + version: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def check(self): + """ + Check the options for any deprecated or soon-to-be-deprecated options. + """ + logger = verboselogs.VerboseLogger(__name__) + logger.addHandler(logging.StreamHandler()) + prev = logger.level + logger.setLevel(verboselogs.ERROR) + + if self.tier: + logger.warning( + "WARNING: Tier is deprecated. Will be removed in a future version." + ) + + if isinstance(self.endpointing, str): + logger.warning( + "WARNING: endpointing's current type previous was `Optional[str]` which is incorrect" + " for backward compatibility we are keeping it as `Optional[Union[str, bool, int]]`" + " since it gets translated to a string to be placed as a query parameter, will keep `str` for now" + " but will change this to `Optional[Union[bool, int]]` in a future release" + ) + + logger.setLevel(prev) + + return True + + +ListenWebSocketOptions = LiveOptions diff --git a/deepgram/clients/listen/v1/websocket/response.py b/deepgram/clients/listen/v1/websocket/response.py new file mode 100644 index 0000000000000000000000000000000000000000..5a086b0c4f0cea06d43704feda572f64f9ee2afa --- /dev/null +++ b/deepgram/clients/listen/v1/websocket/response.py @@ -0,0 +1,217 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Dict, Any + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +# common websocket response +from ....common import ( + BaseResponse, + OpenResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) + +# between rest and websocket +from ....common import ( + ModelInfo, + Hit, + Search, +) + + +# unique + + +@dataclass +class ListenWSWord(BaseResponse): + """ + Word object + """ + + word: str = "" + start: float = 0 + end: float = 0 + confidence: float = 0 + punctuated_word: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + speaker: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + language: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + +@dataclass +class ListenWSAlternative(BaseResponse): + """ + Alternative object + """ + + transcript: str = "" + confidence: float = 0 + words: List[ListenWSWord] = field(default_factory=list) + languages: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "words" in _dict: + _dict["words"] = [ListenWSWord.from_dict(words) for words in _dict["words"]] + return _dict[key] + + +@dataclass +class ListenWSChannel(BaseResponse): + """ + Channel object + """ + + search: Optional[List[Search]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + alternatives: List[ListenWSAlternative] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "search" in _dict: + _dict["search"] = [Search.from_dict(search) for search in _dict["search"]] + if "alternatives" in _dict: + _dict["alternatives"] = [ + ListenWSAlternative.from_dict(alternatives) + for alternatives in _dict["alternatives"] + ] + return _dict[key] + + +@dataclass +class Metadata(BaseResponse): + """ + Metadata object + """ + + model_info: ModelInfo + request_id: str = "" + model_uuid: str = "" + extra: Optional[Dict[str, str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "model_info" in _dict: + _dict["model_info"] = [ + ModelInfo.from_dict(model_info) for model_info in _dict["model_info"] + ] + if "extra" in _dict: + _dict["extra"] = [str(extra) for _, extra in _dict["extra"].items()] + return _dict[key] + + +# live result messages + + +@dataclass +class LiveResultResponse(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + Result Message from the Deepgram Platform + """ + + channel: ListenWSChannel + metadata: Metadata + type: str = "" + channel_index: List[int] = field(default_factory=list) + duration: float = 0 + start: float = 0 + is_final: bool = False + from_finalize: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + speech_final: bool = False + + def __getitem__(self, key): + _dict = self.to_dict() + if "channel" in _dict: + _dict["channel"] = [ + ListenWSChannel.from_dict(channel) for channel in _dict["channel"] + ] + if "metadata" in _dict: + _dict["metadata"] = [ + Metadata.from_dict(metadata) for metadata in _dict["metadata"] + ] + return _dict[key] + + +# Metadata Message + + +@dataclass +class MetadataResponse(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + Metadata Message from the Deepgram Platform + """ + + type: str = "" + transaction_key: str = "" + request_id: str = "" + sha256: str = "" + created: str = "" + duration: float = 0 + channels: int = 0 + models: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + model_info: Optional[Dict[str, ModelInfo]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + extra: Optional[Dict] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "models" in _dict: + _dict["models"] = [str(models) for models in _dict["models"]] + if "model_info" in _dict: + _dict["model_info"] = [ + ModelInfo.from_dict(model_info) + for _, model_info in _dict["model_info"].items() + ] + if "extra" in _dict: + _dict["extra"] = [str(extra) for _, extra in _dict["extra"].items()] + return _dict[key] + + +# Speech Started Message + + +@dataclass +class SpeechStartedResponse(BaseResponse): + """ + SpeechStartedResponse Message from the Deepgram Platform + """ + + type: str = "" + channel: List[int] = field(default_factory=list) + timestamp: float = 0 + + +# Utterance End Message + + +@dataclass +class UtteranceEndResponse(BaseResponse): + """ + UtteranceEnd Message from the Deepgram Platform + """ + + type: str = "" + channel: List[int] = field(default_factory=list) + last_word_end: float = 0 diff --git a/deepgram/clients/listen_router.py b/deepgram/clients/listen_router.py new file mode 100644 index 0000000000000000000000000000000000000000..7bc88bfeaf0bb569432378ffe471b74bdc60b64c --- /dev/null +++ b/deepgram/clients/listen_router.py @@ -0,0 +1,225 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from importlib import import_module +import logging +import deprecation # type: ignore + +from .. import __version__ +from .listen.v1 import ( + PreRecordedClient, + AsyncPreRecordedClient, + LiveClient, + AsyncLiveClient, +) +from ..utils import verboselogs +from ..options import DeepgramClientOptions +from .errors import DeepgramModuleError + + +class ListenRouter: + """ + Represents a client for interacting with the Deepgram API. + + This class provides a client for making requests to the Deepgram API with various configuration options. + + Attributes: + config_options (DeepgramClientOptions): An optional configuration object specifying client options. + + Raises: + DeepgramApiKeyError: If the API key is missing or invalid. + + Methods: + live: (Preferred) Returns a Threaded LiveClient instance for interacting with Deepgram's transcription services. + prerecorded: (Preferred) Returns an Threaded PreRecordedClient instance for interacting with Deepgram's prerecorded transcription services. + + asynclive: Returns an (Async) LiveClient instance for interacting with Deepgram's transcription services. + asyncprerecorded: Returns an (Async) PreRecordedClient instance for interacting with Deepgram's prerecorded transcription services. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + + @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.listen.prerecorded is deprecated. Use deepgram.listen.rest instead.", + ) + def prerecorded(self): + """ + DEPRECATED: deepgram.listen.prerecorded is deprecated. Use deepgram.listen.rest instead. + """ + return self.Version(self._config, "prerecorded") + + @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.listen.asyncprerecorded is deprecated. Use deepgram.listen.asyncrest instead.", + ) + def asyncprerecorded(self): + """ + DEPRECATED: deepgram.listen.asyncprerecorded is deprecated. Use deepgram.listen.asyncrest instead. + """ + return self.Version(self._config, "asyncprerecorded") + + @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.listen.live is deprecated. Use deepgram.listen.websocket instead.", + ) + def live(self): + """ + DEPRECATED: deepgram.listen.live is deprecated. Use deepgram.listen.websocket instead. + """ + return self.Version(self._config, "live") + + @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.listen.asynclive is deprecated. Use deepgram.listen.asyncwebsocket instead.", + ) + def asynclive(self): + """ + DEPRECATED: deepgram.listen.asynclive is deprecated. Use deepgram.listen.asyncwebsocket instead. + """ + return self.Version(self._config, "asynclive") + + @property + def rest(self): + """ + Returns a ListenRESTClient instance for interacting with Deepgram's prerecorded transcription services. + """ + return self.Version(self._config, "rest") + + @property + def asyncrest(self): + """ + Returns an AsyncListenRESTClient instance for interacting with Deepgram's prerecorded transcription services. + """ + return self.Version(self._config, "asyncrest") + + @property + def websocket(self): + """ + Returns a ListenWebSocketClient instance for interacting with Deepgram's transcription services. + """ + return self.Version(self._config, "websocket") + + @property + def asyncwebsocket(self): + """ + Returns an AsyncListenWebSocketClient instance for interacting with Deepgram's transcription services. + """ + return self.Version(self._config, "asyncwebsocket") + + # INTERNAL CLASSES + class Version: + """ + Represents a version of the Deepgram API. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _parent: str + + def __init__(self, config, parent: str): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._parent = parent + + # FUTURE VERSIONING: + # When v2 or v1.1beta1 or etc. This allows easy access to the latest version of the API. + # @property + # def latest(self): + # match self._parent: + # case "live": + # return LiveClient(self._config) + # case "prerecorded": + # return PreRecordedClient(self._config) + # case _: + # raise DeepgramModuleError("Invalid parent") + + def v(self, version: str = ""): + """ + Returns a specific version of the Deepgram API. + """ + self._logger.debug("Version.v ENTER") + self._logger.info("version: %s", version) + if len(version) == 0: + self._logger.error("version is empty") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid module version") + + protocol = "" + file_name = "" + class_name = "" + match self._parent: + case "live": + return LiveClient(self._config) + case "asynclive": + return AsyncLiveClient(self._config) + case "prerecorded": + return PreRecordedClient(self._config) + case "asyncprerecorded": + return AsyncPreRecordedClient(self._config) + case "websocket": + protocol = "websocket" + file_name = "client" + class_name = "ListenWebSocketClient" + case "asyncwebsocket": + protocol = "websocket" + file_name = "async_client" + class_name = "AsyncListenWebSocketClient" + case "rest": + protocol = "rest" + file_name = "client" + class_name = "ListenRESTClient" + case "asyncrest": + protocol = "rest" + file_name = "async_client" + class_name = "AsyncListenRESTClient" + case _: + self._logger.error("parent unknown: %s", self._parent) + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid parent type") + + # create class path + path = f"deepgram.clients.listen.v{version}.{protocol}.{file_name}" + self._logger.info("path: %s", path) + self._logger.info("class_name: %s", class_name) + + # import class + mod = import_module(path) + if mod is None: + self._logger.error("module path is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find package") + + my_class = getattr(mod, class_name) + if my_class is None: + self._logger.error("my_class is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find class") + + # instantiate class + my_class = my_class(self._config) + self._logger.notice("Version.v succeeded") + self._logger.debug("Version.v LEAVE") + return my_class diff --git a/deepgram/clients/live/__init__.py b/deepgram/clients/live/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e71ab0073a192f0b45ffe619c012e42a1d3419c4 --- /dev/null +++ b/deepgram/clients/live/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .v1 import LiveTranscriptionEvents + +from .v1 import LiveClient +from .v1 import AsyncLiveClient +from .v1 import LiveOptions +from .v1 import ( + OpenResponse, + LiveResultResponse, + ListenWSMetadataResponse, + MetadataResponse, # backwards compat + SpeechStartedResponse, + UtteranceEndResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) diff --git a/deepgram/clients/live/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/live/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4ba6f215da70c1c4c1baba4319e5409e077bc4a Binary files /dev/null and b/deepgram/clients/live/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/live/v1/__init__.py b/deepgram/clients/live/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..273c4d3b50caf45b48fa2487e6a72e68b5860b36 --- /dev/null +++ b/deepgram/clients/live/v1/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .enums import LiveTranscriptionEvents + +from .client import LiveClient +from .client import AsyncLiveClient +from .client import LiveOptions +from .client import ( + OpenResponse, + LiveResultResponse, + ListenWSMetadataResponse, + MetadataResponse, # backwards compat + SpeechStartedResponse, + UtteranceEndResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) diff --git a/deepgram/clients/live/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/live/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..916175bb6f032327caba4380e5ec176c71af2b06 Binary files /dev/null and b/deepgram/clients/live/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/live/v1/__pycache__/client.cpython-313.pyc b/deepgram/clients/live/v1/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88f54129464e11d7541ef397c79296bfd81bb46b Binary files /dev/null and b/deepgram/clients/live/v1/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/live/v1/__pycache__/enums.cpython-313.pyc b/deepgram/clients/live/v1/__pycache__/enums.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..076ffd508e709919dbfac28390cc8272295a901c Binary files /dev/null and b/deepgram/clients/live/v1/__pycache__/enums.cpython-313.pyc differ diff --git a/deepgram/clients/live/v1/client.py b/deepgram/clients/live/v1/client.py new file mode 100644 index 0000000000000000000000000000000000000000..fd75c9487906bbdeea5bdeecb272ed8733b2b187 --- /dev/null +++ b/deepgram/clients/live/v1/client.py @@ -0,0 +1,38 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from ...listen.v1 import ListenWebSocketClient as LiveClientLatest +from ...listen.v1 import AsyncListenWebSocketClient as AsyncLiveClientLatest +from ...listen.v1 import LiveOptions as LiveOptionsLatest +from ...listen.v1 import ( + OpenResponse as OpenResponseLatest, + LiveResultResponse as LiveResultResponseLatest, + ListenWSMetadataResponse as ListenWSMetadataResponseLatest, + SpeechStartedResponse as SpeechStartedResponseLatest, + UtteranceEndResponse as UtteranceEndResponseLatest, + CloseResponse as CloseResponseLatest, + ErrorResponse as ErrorResponseLatest, + UnhandledResponse as UnhandledResponseLatest, +) + +# The vX/client.py points to the current supported version in the SDK. +# Older versions are supported in the SDK for backwards compatibility. + + +# input +LiveOptions = LiveOptionsLatest +OpenResponse = OpenResponseLatest +LiveResultResponse = LiveResultResponseLatest +ListenWSMetadataResponse = ListenWSMetadataResponseLatest +MetadataResponse = ListenWSMetadataResponseLatest +SpeechStartedResponse = SpeechStartedResponseLatest +UtteranceEndResponse = UtteranceEndResponseLatest +CloseResponse = CloseResponseLatest +ErrorResponse = ErrorResponseLatest +UnhandledResponse = UnhandledResponseLatest + + +# clients +LiveClient = LiveClientLatest +AsyncLiveClient = AsyncLiveClientLatest diff --git a/deepgram/clients/live/v1/enums.py b/deepgram/clients/live/v1/enums.py new file mode 100644 index 0000000000000000000000000000000000000000..ee98540ffc8227cc1cd121bb171d6876ceb15b04 --- /dev/null +++ b/deepgram/clients/live/v1/enums.py @@ -0,0 +1,5 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from ...listen import LiveTranscriptionEvents diff --git a/deepgram/clients/manage/__init__.py b/deepgram/clients/manage/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8a4287ae85d4912e77c33d60f685f442d8b124be --- /dev/null +++ b/deepgram/clients/manage/__init__.py @@ -0,0 +1,53 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import ManageClient +from .client import AsyncManageClient +from .client import ( + ProjectOptions, + KeyOptions, + ScopeOptions, + InviteOptions, + UsageRequestOptions, + UsageSummaryOptions, + UsageFieldsOptions, +) +from .client import ( + #### top level + Message, + ProjectsResponse, + ModelResponse, + ModelsResponse, + MembersResponse, + KeyResponse, + KeysResponse, + ScopesResponse, + InvitesResponse, + UsageRequest, + UsageResponse, + UsageRequestsResponse, + UsageSummaryResponse, + UsageFieldsResponse, + BalancesResponse, + #### shared + Project, + STTDetails, + TTSMetadata, + TTSDetails, + Member, + Key, + Invite, + Config, + STTUsageDetails, + Callback, + TokenDetail, + SpeechSegment, + TTSUsageDetails, + STTTokens, + TTSTokens, + UsageSummaryResults, + Resolution, + UsageModel, + Balance, +) diff --git a/deepgram/clients/manage/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/manage/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d2156784135e4c31d5829084055e7d090617a48 Binary files /dev/null and b/deepgram/clients/manage/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/manage/__pycache__/client.cpython-313.pyc b/deepgram/clients/manage/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3b02b0ac19219c560afc0961f2d4fe0889fb040 Binary files /dev/null and b/deepgram/clients/manage/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/manage/client.py b/deepgram/clients/manage/client.py new file mode 100644 index 0000000000000000000000000000000000000000..8238cd7ad0788a5315a38cc5be1cff5410cef3eb --- /dev/null +++ b/deepgram/clients/manage/client.py @@ -0,0 +1,110 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .v1.client import ManageClient as ManageClientLatest +from .v1.async_client import AsyncManageClient as AsyncManageClientLatest + +# input +from .v1.options import ( + ProjectOptions as ProjectOptionsLatest, + KeyOptions as KeyOptionsLatest, + ScopeOptions as ScopeOptionsLatest, + InviteOptions as InviteOptionsLatest, + UsageRequestOptions as UsageRequestOptionsLatest, + UsageSummaryOptions as UsageSummaryOptionsLatest, + UsageFieldsOptions as UsageFieldsOptionsLatest, +) + +# responses +from .v1.response import ( + Message as MessageLatest, + ProjectsResponse as ProjectsResponseLatest, + ModelResponse as ModelResponseLatest, + ModelsResponse as ModelsResponseLatest, + MembersResponse as MembersResponseLatest, + KeyResponse as KeyResponseLatest, + KeysResponse as KeysResponseLatest, + ScopesResponse as ScopesResponseLatest, + InvitesResponse as InvitesResponseLatest, + UsageRequest as UsageRequestLatest, + UsageResponse as UsageResponseLatest, + UsageRequestsResponse as UsageRequestsResponseLatest, + UsageSummaryResponse as UsageSummaryResponseLatest, + UsageFieldsResponse as UsageFieldsResponseLatest, + BalancesResponse as BalancesResponseLatest, + Project as ProjectLatest, + STTDetails as STTDetailsLatest, + TTSMetadata as TTSMetadataLatest, + TTSDetails as TTSDetailsLatest, + Member as MemberLatest, + Key as KeyLatest, + Invite as InviteLatest, + Config as ConfigLatest, + STTUsageDetails as STTUsageDetailsLatest, + Callback as CallbackLatest, + TokenDetail as TokenDetailLatest, + SpeechSegment as SpeechSegmentLatest, + TTSUsageDetails as TTSUsageDetailsLatest, + STTTokens as STTTokensLatest, + TTSTokens as TTSTokensLatest, + UsageSummaryResults as UsageSummaryResultsLatest, + Resolution as ResolutionLatest, + UsageModel as UsageModelLatest, + Balance as BalanceLatest, +) + + +# The client.py points to the current supported version in the SDK. +# Older versions are supported in the SDK for backwards compatibility. + + +# input +ProjectOptions = ProjectOptionsLatest +KeyOptions = KeyOptionsLatest +ScopeOptions = ScopeOptionsLatest +InviteOptions = InviteOptionsLatest +UsageRequestOptions = UsageRequestOptionsLatest +UsageSummaryOptions = UsageSummaryOptionsLatest +UsageFieldsOptions = UsageFieldsOptionsLatest + + +# responses +Message = MessageLatest +ProjectsResponse = ProjectsResponseLatest +ModelResponse = ModelResponseLatest +ModelsResponse = ModelsResponseLatest +MembersResponse = MembersResponseLatest +KeyResponse = KeyResponseLatest +KeysResponse = KeysResponseLatest +ScopesResponse = ScopesResponseLatest +InvitesResponse = InvitesResponseLatest +UsageRequest = UsageRequestLatest +UsageResponse = UsageResponseLatest +UsageRequestsResponse = UsageRequestsResponseLatest +UsageSummaryResponse = UsageSummaryResponseLatest +UsageFieldsResponse = UsageFieldsResponseLatest +BalancesResponse = BalancesResponseLatest +Project = ProjectLatest +STTDetails = STTDetailsLatest +TTSMetadata = TTSMetadataLatest +TTSDetails = TTSDetailsLatest +Member = MemberLatest +Key = KeyLatest +Invite = InviteLatest +Config = ConfigLatest +STTUsageDetails = STTUsageDetailsLatest +Callback = CallbackLatest +TokenDetail = TokenDetailLatest +SpeechSegment = SpeechSegmentLatest +TTSUsageDetails = TTSUsageDetailsLatest +STTTokens = STTTokensLatest +TTSTokens = TTSTokensLatest +UsageSummaryResults = UsageSummaryResultsLatest +Resolution = ResolutionLatest +UsageModel = UsageModelLatest +Balance = BalanceLatest + +# clients +ManageClient = ManageClientLatest +AsyncManageClient = AsyncManageClientLatest diff --git a/deepgram/clients/manage/v1/__init__.py b/deepgram/clients/manage/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0373a56e4a01594704266bcec31c530f308be12a --- /dev/null +++ b/deepgram/clients/manage/v1/__init__.py @@ -0,0 +1,54 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import ManageClient +from .async_client import AsyncManageClient +from .options import ( + ProjectOptions, + KeyOptions, + ScopeOptions, + InviteOptions, + UsageRequestOptions, + UsageSummaryOptions, + UsageFieldsOptions, +) + +from .response import ( + #### top level + Message, + ProjectsResponse, + ModelResponse, + ModelsResponse, + MembersResponse, + KeyResponse, + KeysResponse, + ScopesResponse, + InvitesResponse, + UsageRequest, + UsageResponse, + UsageRequestsResponse, + UsageSummaryResponse, + UsageFieldsResponse, + BalancesResponse, + #### shared + Project, + STTDetails, + TTSMetadata, + TTSDetails, + Member, + Key, + Invite, + Config, + STTUsageDetails, + Callback, + TokenDetail, + SpeechSegment, + TTSUsageDetails, + STTTokens, + TTSTokens, + UsageSummaryResults, + Resolution, + UsageModel, + Balance, +) diff --git a/deepgram/clients/manage/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/manage/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b0e766640ea42be16933996c718ace9e3bddd04 Binary files /dev/null and b/deepgram/clients/manage/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/manage/v1/__pycache__/async_client.cpython-313.pyc b/deepgram/clients/manage/v1/__pycache__/async_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fcfc762fa4763894caf43455e177a40b3afaec4 Binary files /dev/null and b/deepgram/clients/manage/v1/__pycache__/async_client.cpython-313.pyc differ diff --git a/deepgram/clients/manage/v1/__pycache__/client.cpython-313.pyc b/deepgram/clients/manage/v1/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..442ddb3606a599c2eb04b5ad5bb1ddc5fb55324f Binary files /dev/null and b/deepgram/clients/manage/v1/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/manage/v1/__pycache__/options.cpython-313.pyc b/deepgram/clients/manage/v1/__pycache__/options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b057f418aea59e3ec6826eff725aacc09375a3d Binary files /dev/null and b/deepgram/clients/manage/v1/__pycache__/options.cpython-313.pyc differ diff --git a/deepgram/clients/manage/v1/__pycache__/response.cpython-313.pyc b/deepgram/clients/manage/v1/__pycache__/response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98054bc812b3d06a709a6a6819e3d9f14816f565 Binary files /dev/null and b/deepgram/clients/manage/v1/__pycache__/response.cpython-313.pyc differ diff --git a/deepgram/clients/manage/v1/async_client.py b/deepgram/clients/manage/v1/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..99fea77a5b37271ae9847c7f9e3199a31713c65b --- /dev/null +++ b/deepgram/clients/manage/v1/async_client.py @@ -0,0 +1,1176 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Dict, Union, Optional +import json + +import httpx + +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from ...common import AbstractAsyncRestClient, DeepgramError + +from .response import ( + Message, + Project, + ProjectsResponse, + MembersResponse, + Key, + KeyResponse, + KeysResponse, + ScopesResponse, + InvitesResponse, + UsageRequest, + UsageRequestsResponse, + UsageSummaryResponse, + UsageFieldsResponse, + Balance, + BalancesResponse, + ModelResponse, + ModelsResponse, +) +from .options import ( + ProjectOptions, + ModelOptions, + KeyOptions, + ScopeOptions, + InviteOptions, + UsageRequestOptions, + UsageSummaryOptions, + UsageFieldsOptions, +) + + +class AsyncManageClient( + AbstractAsyncRestClient +): # pylint: disable=too-many-public-methods,too-many-lines + """ + A client for managing Deepgram projects and associated resources via the Deepgram API. + + This class provides methods for performing various operations on Deepgram projects, including: + - Retrieving project details + - Updating project settings + - Managing project members and scopes + - Handling project invitations + - Monitoring project usage and balances + + Args: + config (DeepgramClientOptions): all the options for the client. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._endpoint = "v1/projects" + super().__init__(config) + + # pylint: disable=too-many-positional-arguments + + # projects + async def list_projects( + self, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ProjectsResponse: + """ + Please see get_projects for more information. + """ + return await self.get_projects( + timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + async def get_projects( + self, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ProjectsResponse: + """ + Gets a list of projects for the authenticated user. + + Reference: + https://developers.deepgram.com/reference/get-projects + """ + self._logger.debug("ManageClient.get_projects ENTER") + url = f"{self._config.url}/{self._endpoint}" + self._logger.info("url: %s", url) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = ProjectsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_projects succeeded") + self._logger.debug("ManageClient.get_projects LEAVE") + return res + + async def get_project( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Project: + """ + Gets details for a specific project. + + Reference: + https://developers.deepgram.com/reference/get-project + """ + self._logger.debug("ManageClient.get_project ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Project.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_project succeeded") + self._logger.debug("ManageClient.get_project LEAVE") + return res + + async def update_project_option( + self, + project_id: str, + options: Union[Dict, ProjectOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Updates a project's settings. + + Reference: + https://developers.deepgram.com/reference/update-project + """ + self._logger.debug("ManageClient.update_project_option ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, ProjectOptions): + self._logger.info("ProjectOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.patch( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("update_project_option succeeded") + self._logger.debug("ManageClient.update_project_option LEAVE") + return res + + async def update_project( + self, + project_id: str, + name="", + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Updates a project's settings. + + Reference: + https://developers.deepgram.com/reference/update-project + """ + self._logger.debug("ManageClient.update_project ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}" + options = { + "name": name, + } + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.patch( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("update_project succeeded") + self._logger.debug("ManageClient.update_project LEAVE") + return res + + async def delete_project( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Deletes a project. + + Reference: + https://developers.deepgram.com/reference/delete-project + """ + self._logger.debug("ManageClient.delete_project ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}" + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("delete_project succeeded") + self._logger.debug("ManageClient.delete_project LEAVE") + return res + + async def list_project_models( + self, + project_id: str, + options: Optional[Union[Dict, ModelOptions]] = None, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelsResponse: + """ + Please see get_project_models. + """ + return await self.get_project_models( + project_id, + options=options, + timeout=timeout, + addons=addons, + headers=headers, + **kwargs, + ) + + async def get_project_models( + self, + project_id: str, + options: Optional[Union[Dict, ModelOptions]] = None, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelsResponse: + """ + Gets models for a specific project. + + Reference: + https://developers.deepgram.com/reference/get-project + https://developers.deepgram.com/reference/get-model + + Args: + project_id (str): The ID of the project. + timeout (Optional[httpx.Timeout]): The timeout setting for the request. + addons (Optional[Dict]): Additional options for the request. + headers (Optional[Dict]): Headers to include in the request. + **kwargs: Additional keyword arguments. + + Returns: + ModelsResponse: A response object containing the model details. + """ + self._logger.debug("ManageClient.get_project_models ENTER") + + if options is None: + options = ModelOptions() + + url = f"{self._config.url}/{self._endpoint}/{project_id}/models" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, ModelOptions): + self._logger.info("ModelOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = ModelsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_project_models succeeded") + self._logger.debug("ManageClient.get_project_models LEAVE") + return res + + async def get_project_model( + self, + project_id: str, + model_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelResponse: + """ + Gets a single model for a specific project. + + Reference: + https://developers.deepgram.com/reference/get-project + https://developers.deepgram.com/reference/get-model + + Args: + project_id (str): The ID of the project. + model_id (str): The ID of the model. + timeout (Optional[httpx.Timeout]): The timeout setting for the request. + addons (Optional[Dict]): Additional options for the request. + headers (Optional[Dict]): Headers to include in the request. + **kwargs: Additional keyword arguments. + + Returns: + ModelResponse: A response object containing the model details. + """ + self._logger.debug("ManageClient.get_project_model ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/models/{model_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("model_id: %s", model_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = ModelResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_project_model succeeded") + self._logger.debug("ManageClient.get_project_model LEAVE") + return res + + # models + async def list_models( + self, + options: Optional[Union[Dict, ModelOptions]] = None, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelsResponse: + """ + Please see get_models for more information. + """ + return await self.get_models( + options=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + async def get_models( + self, + options: Optional[Union[Dict, ModelOptions]] = None, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelsResponse: + """ + Gets all models available. + + Reference: + https://developers.deepgram.com/reference/get-model + + Args: + timeout (Optional[httpx.Timeout]): The timeout setting for the request. + addons (Optional[Dict]): Additional options for the request. + headers (Optional[Dict]): Headers to include in the request. + **kwargs: Additional keyword arguments. + + Returns: + ModelsResponse: A response object containing the model details. + """ + self._logger.debug("ManageClient.get_models ENTER") + + if options is None: + options = ModelOptions() + + url = f"{self._config.url}/v1/models" + self._logger.info("url: %s", url) + if isinstance(options, ModelOptions): + self._logger.info("ModelOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = ModelsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_models succeeded") + self._logger.debug("ManageClient.get_models LEAVE") + return res + + async def get_model( + self, + model_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelResponse: + """ + Gets information for a specific model. + + Reference: + https://developers.deepgram.com/reference/get-model + + Args: + model_id (str): The ID of the model. + timeout (Optional[httpx.Timeout]): The timeout setting for the request. + addons (Optional[Dict]): Additional options for the request. + headers (Optional[Dict]): Headers to include in the request. + **kwargs: Additional keyword arguments. + + Returns: + ModelResponse: A response object containing the model details. + """ + self._logger.debug("ManageClient.get_model ENTER") + url = f"{self._config.url}/v1/models/{model_id}" + self._logger.info("url: %s", url) + self._logger.info("model_id: %s", model_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = ModelResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_model succeeded") + self._logger.debug("ManageClient.get_model LEAVE") + return res + + # keys + async def list_keys( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> KeysResponse: + """ + Please see get_keys for more information. + """ + return await self.get_keys( + project_id, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + async def get_keys( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> KeysResponse: + """ + Gets a list of keys for a project. + + Reference: + https://developers.deepgram.com/reference/list-keys + """ + self._logger.debug("ManageClient.get_keys ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/keys" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = KeysResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_keys succeeded") + self._logger.debug("ManageClient.get_keys LEAVE") + return res + + async def get_key( + self, + project_id: str, + key_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> KeyResponse: + """ + Gets details for a specific key. + + Reference: + https://developers.deepgram.com/reference/get-key + """ + self._logger.debug("ManageClient.get_key ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/keys/{key_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("key_id: %s", key_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = KeyResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_key succeeded") + self._logger.debug("ManageClient.get_key LEAVE") + return res + + async def create_key( + self, + project_id: str, + options: Union[Dict, KeyOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Key: + """ + Creates a new key. + + Reference: + https://developers.deepgram.com/reference/create-key + """ + self._logger.debug("ManageClient.create_key ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/keys" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, KeyOptions): + self._logger.info("KeyOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Key.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("create_key succeeded") + self._logger.debug("ManageClient.create_key LEAVE") + return res + + async def delete_key( + self, + project_id: str, + key_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Deletes a key. + + Reference: + https://developers.deepgram.com/reference/delete-key + """ + self._logger.debug("ManageClient.delete_key ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/keys/{key_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("key_id: %s", key_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("delete_key succeeded") + self._logger.debug("ManageClient.delete_key LEAVE") + return res + + # members + async def list_members( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> MembersResponse: + """ + Please see get_members for more information. + """ + return await self.get_members( + project_id, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + async def get_members( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> MembersResponse: + """ + Gets a list of members for a project. + + Reference: + https://developers.deepgram.com/reference/get-members + """ + self._logger.debug("ManageClient.get_members ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/members" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = MembersResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_members succeeded") + self._logger.debug("ManageClient.get_members LEAVE") + return res + + async def remove_member( + self, + project_id: str, + member_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Removes a member from a project. + + Reference: + https://developers.deepgram.com/reference/remove-member + """ + self._logger.debug("ManageClient.remove_member ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/members/{member_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("member_id: %s", member_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("remove_member succeeded") + self._logger.debug("ManageClient.remove_member LEAVE") + return res + + # scopes + async def get_member_scopes( + self, + project_id: str, + member_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ScopesResponse: + """ + Gets a list of scopes for a member. + + Reference: + https://developers.deepgram.com/reference/get-member-scopes + """ + self._logger.debug("ManageClient.get_member_scopes ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/members/{member_id}/scopes" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("member_id: %s", member_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = ScopesResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_member_scopes succeeded") + self._logger.debug("ManageClient.get_member_scopes LEAVE") + return res + + async def update_member_scope( + self, + project_id: str, + member_id: str, + options: Union[Dict, ScopeOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Updates a member's scopes. + + Reference: + https://developers.deepgram.com/reference/update-scope + """ + self._logger.debug("ManageClient.update_member_scope ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/members/{member_id}/scopes" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, ScopeOptions): + self._logger.info("ScopeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.put( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("update_member_scope succeeded") + self._logger.debug("ManageClient.update_member_scope LEAVE") + return res + + # invites + async def list_invites( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> InvitesResponse: + """ + Please see get_invites for more information. + """ + return await self.get_invites( + project_id, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + async def get_invites( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> InvitesResponse: + """ + Gets a list of invites for a project. + + Reference: + https://developers.deepgram.com/reference/list-invites + """ + self._logger.debug("ManageClient.get_invites ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/invites" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = InvitesResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_invites succeeded") + self._logger.debug("ManageClient.get_invites LEAVE") + return res + + async def send_invite_options( + self, + project_id: str, + options: Union[Dict, InviteOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Sends an invite to a project. + + Reference: + https://developers.deepgram.com/reference/send-invite + """ + self._logger.debug("ManageClient.send_invite_options ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/invites" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, InviteOptions): + self._logger.info("InviteOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("send_invite_options succeeded") + self._logger.debug("ManageClient.send_invite_options LEAVE") + return res + + async def send_invite( + self, + project_id: str, + email: str, + scope="member", + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Sends an invite to a project. + + Reference: + https://developers.deepgram.com/reference/send-invite + """ + self._logger.debug("ManageClient.send_invite ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/invites" + options = { + "email": email, + "scope": scope, + } + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.post( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("send_invite succeeded") + self._logger.debug("ManageClient.send_invite LEAVE") + return res + + async def delete_invite( + self, + project_id: str, + email: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Deletes an invite from a project. + + Reference: + https://developers.deepgram.com/reference/delete-invite + """ + self._logger.debug("ManageClient.delete_invite ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/invites/{email}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("email: %s", email) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("delete_invite succeeded") + self._logger.debug("ManageClient.delete_invite LEAVE") + return res + + async def leave_project( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Leaves a project. + + Reference: + https://developers.deepgram.com/reference/leave-project + """ + self._logger.debug("ManageClient.leave_project ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/leave" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("leave_project succeeded") + self._logger.debug("ManageClient.leave_project LEAVE") + return res + + # usage + async def get_usage_requests( + self, + project_id: str, + options: Union[Dict, UsageRequestOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> UsageRequestsResponse: + """ + Gets a list of usage requests for a project. + + Reference: + https://developers.deepgram.com/reference/get-all-requests + """ + self._logger.debug("ManageClient.get_usage_requests ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/requests" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, UsageRequestOptions): + self._logger.info("UsageRequestOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, + options=options, + timeout=timeout, + addons=addons, + headers=headers, + **kwargs, + ) + self._logger.info("result: %s", result) + res = UsageRequestsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_usage_requests succeeded") + self._logger.debug("ManageClient.get_usage_requests LEAVE") + return res + + async def get_usage_request( + self, + project_id: str, + request_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> UsageRequest: + """ + Gets details for a specific usage request. + + Reference: + https://developers.deepgram.com/reference/get-request + """ + self._logger.debug("ManageClient.get_usage_request ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/requests/{request_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("request_id: %s", request_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + + # convert str to JSON to check response field + json_result = json.loads(result) + if json_result.get("response") is None: + raise DeepgramError( + "Response is not available yet. Please try again later." + ) + + res = UsageRequest.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_usage_request succeeded") + self._logger.debug("ManageClient.get_usage_request LEAVE") + return res + + async def get_usage_summary( + self, + project_id: str, + options: Union[Dict, UsageSummaryOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> UsageSummaryResponse: + """ + Gets a summary of usage for a project. + + Reference: + https://developers.deepgram.com/reference/summarize-usage + """ + self._logger.debug("ManageClient.get_usage_summary ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/usage" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, UsageSummaryOptions): + self._logger.info("UsageSummaryOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, + options=options, + timeout=timeout, + addons=addons, + headers=headers, + **kwargs, + ) + self._logger.info("result: %s", result) + res = UsageSummaryResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_usage_summary succeeded") + self._logger.debug("ManageClient.get_usage_summary LEAVE") + return res + + async def get_usage_fields( + self, + project_id: str, + options: Union[Dict, UsageFieldsOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> UsageFieldsResponse: + """ + Gets a list of usage fields for a project. + + Reference: + https://developers.deepgram.com/reference/get-fields + """ + self._logger.debug("ManageClient.get_usage_fields ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/usage/fields" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, UsageFieldsOptions): + self._logger.info("UsageFieldsOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, + options=options, + timeout=timeout, + addons=addons, + headers=headers, + **kwargs, + ) + self._logger.info("result: %s", result) + res = UsageFieldsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_usage_fields succeeded") + self._logger.debug("ManageClient.get_usage_fields LEAVE") + return res + + # balances + async def list_balances( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> BalancesResponse: + """ + Please see get_balances for more information. + """ + return await self.get_balances( + project_id, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + async def get_balances( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> BalancesResponse: + """ + Gets a list of balances for a project. + + Reference: + https://developers.deepgram.com/reference/get-all-balances + """ + self._logger.debug("ManageClient.get_balances ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/balances" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = BalancesResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_balances succeeded") + self._logger.debug("ManageClient.get_balances LEAVE") + return res + + async def get_balance( + self, + project_id: str, + balance_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Balance: + """ + Gets details for a specific balance. + + Reference: + https://developers.deepgram.com/reference/get-balance + """ + self._logger.debug("ManageClient.get_balance ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/balances/{balance_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("balance_id: %s", balance_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = await self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("result: %s", result) + res = Balance.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_balance succeeded") + self._logger.debug("ManageClient.get_balance LEAVE") + return res + + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/manage/v1/client.py b/deepgram/clients/manage/v1/client.py new file mode 100644 index 0000000000000000000000000000000000000000..a2b6e94c6579d4f70dd59fcd90bed0f9b780ca07 --- /dev/null +++ b/deepgram/clients/manage/v1/client.py @@ -0,0 +1,1179 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Dict, Union, Optional +import json + +import httpx + +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from ...common import AbstractSyncRestClient, DeepgramError + +from .response import ( + Message, + Project, + ProjectsResponse, + MembersResponse, + Key, + KeyResponse, + KeysResponse, + ScopesResponse, + InvitesResponse, + UsageRequest, + UsageRequestsResponse, + UsageSummaryResponse, + UsageFieldsResponse, + Balance, + BalancesResponse, + ModelResponse, + ModelsResponse, +) +from .options import ( + ProjectOptions, + ModelOptions, + KeyOptions, + ScopeOptions, + InviteOptions, + UsageRequestOptions, + UsageSummaryOptions, + UsageFieldsOptions, +) + + +class ManageClient( + AbstractSyncRestClient +): # pylint: disable=too-many-public-methods,too-many-lines + """ + A client for managing Deepgram projects and associated resources via the Deepgram API. + + This class provides methods for performing various operations on Deepgram projects, including: + - Retrieving project details + - Updating project settings + - Managing project members and scopes + - Handling project invitations + - Monitoring project usage and balances + + Args: + config (DeepgramClientOptions): all the options for the client. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + self._endpoint = "v1/projects" + super().__init__(config) + + # projects + def list_projects( + self, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ProjectsResponse: + """ + List all projects for the current user. + """ + return self.get_projects( + timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + # pylint: disable=too-many-positional-arguments + + def get_projects( + self, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ProjectsResponse: + """ + Gets a list of projects for the authenticated user. + + Reference: + https://developers.deepgram.com/reference/get-projects + """ + self._logger.debug("ManageClient.get_projects ENTER") + url = f"{self._config.url}/{self._endpoint}" + self._logger.info("url: %s", url) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = ProjectsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_projects succeeded") + self._logger.debug("ManageClient.get_projects LEAVE") + return res + + def get_project( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Project: + """ + Gets details for a specific project. + + Reference: + https://developers.deepgram.com/reference/get-project + """ + self._logger.debug("ManageClient.get_project ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Project.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_project succeeded") + self._logger.debug("ManageClient.get_project LEAVE") + return res + + def update_project_option( + self, + project_id: str, + options: Union[Dict, ProjectOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Updates a project's settings. + + Reference: + https://developers.deepgram.com/reference/update-project + """ + self._logger.debug("ManageClient.update_project_option ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, ProjectOptions): + self._logger.info("ProjectOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.patch( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("update_project_option succeeded") + self._logger.debug("ManageClient.update_project_option LEAVE") + return res + + def update_project( + self, + project_id: str, + name: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Updates a project's settings. + + Reference: + https://developers.deepgram.com/reference/update-project + """ + self._logger.debug("ManageClient.update_project ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}" + options = { + "name": name, + } + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.patch( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("update_project succeeded") + self._logger.debug("ManageClient.update_project LEAVE") + return res + + def delete_project( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Deletes a project. + + Reference: + https://developers.deepgram.com/reference/delete-project + """ + self._logger.debug("ManageClient.delete_project ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}" + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("delete_project succeeded") + self._logger.debug("ManageClient.delete_project LEAVE") + return res + + def list_project_models( + self, + project_id: str, + options: Optional[Union[Dict, ModelOptions]] = None, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelsResponse: + """ + Please see get_project_models. + """ + return self.get_project_models( + project_id, + options=options, + timeout=timeout, + addons=addons, + headers=headers, + **kwargs, + ) + + def get_project_models( + self, + project_id: str, + options: Optional[Union[Dict, ModelOptions]] = None, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelsResponse: + """ + Gets models for a specific project. + + Reference: + https://developers.deepgram.com/reference/get-project + https://developers.deepgram.com/reference/get-model + + Args: + project_id (str): The ID of the project. + options (Optional[Union[Dict, ModelOptions]]): The options for the request. + timeout (Optional[httpx.Timeout]): The timeout setting for the request. + addons (Optional[Dict]): Additional options for the request. + headers (Optional[Dict]): Headers to include in the request. + **kwargs: Additional keyword arguments. + + Returns: + ModelsResponse: A response object containing the model details. + """ + self._logger.debug("ManageClient.get_project_models ENTER") + + if options is None: + options = ModelOptions() + + url = f"{self._config.url}/{self._endpoint}/{project_id}/models" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, ModelOptions): + self._logger.info("ModelOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = ModelsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_project_models succeeded") + self._logger.debug("ManageClient.get_project_models LEAVE") + return res + + def get_project_model( + self, + project_id: str, + model_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelResponse: + """ + Gets a single model for a specific project. + + Reference: + https://developers.deepgram.com/reference/get-project + https://developers.deepgram.com/reference/get-model + + Args: + project_id (str): The ID of the project. + model_id (str): The ID of the model. + timeout (Optional[httpx.Timeout]): The timeout setting for the request. + addons (Optional[Dict]): Additional options for the request. + headers (Optional[Dict]): Headers to include in the request. + **kwargs: Additional keyword arguments. + + Returns: + ModelResponse: A response object containing the model details. + """ + self._logger.debug("ManageClient.get_project_model ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/models/{model_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("model_id: %s", model_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = ModelResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_project_model succeeded") + self._logger.debug("ManageClient.get_project_model LEAVE") + return res + + # models + def list_models( + self, + options: Optional[Union[Dict, ModelOptions]] = None, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelsResponse: + """ + Please see get_models for more information. + """ + return self.get_models( + options=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + def get_models( + self, + options: Optional[Union[Dict, ModelOptions]] = None, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelsResponse: + """ + Gets all models available. + + Reference: + https://developers.deepgram.com/reference/get-model + + Args: + options (Optional[Union[Dict, ModelOptions]]): The options for the request. + timeout (Optional[httpx.Timeout]): The timeout setting for the request. + addons (Optional[Dict]): Additional options for the request. + headers (Optional[Dict]): Headers to include in the request. + **kwargs: Additional keyword arguments. + + Returns: + ModelsResponse: A response object containing the model details. + """ + self._logger.debug("ManageClient.get_models ENTER") + + if options is None: + options = ModelOptions() + + url = f"{self._config.url}/v1/models" + self._logger.info("url: %s", url) + if isinstance(options, ModelOptions): + self._logger.info("ModelOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = ModelsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_models succeeded") + self._logger.debug("ManageClient.get_models LEAVE") + return res + + def get_model( + self, + model_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ModelResponse: + """ + Gets information for a specific model. + + Reference: + https://developers.deepgram.com/reference/get-model + + Args: + model_id (str): The ID of the model. + timeout (Optional[httpx.Timeout]): The timeout setting for the request. + addons (Optional[Dict]): Additional options for the request. + headers (Optional[Dict]): Headers to include in the request. + **kwargs: Additional keyword arguments. + + Returns: + ModelResponse: A response object containing the model details. + """ + self._logger.debug("ManageClient.get_model ENTER") + url = f"{self._config.url}/v1/models/{model_id}" + self._logger.info("url: %s", url) + self._logger.info("model_id: %s", model_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = ModelResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_model succeeded") + self._logger.debug("ManageClient.get_model LEAVE") + return res + + # keys + def list_keys( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> KeysResponse: + """ + Please see get_keys for more information. + """ + return self.get_keys( + project_id, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + def get_keys( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> KeysResponse: + """ + Gets a list of keys for a project. + + Reference: + https://developers.deepgram.com/reference/list-keys + """ + self._logger.debug("ManageClient.get_keys ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/keys" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = KeysResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_keys succeeded") + self._logger.debug("ManageClient.get_keys LEAVE") + return res + + def get_key( + self, + project_id: str, + key_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> KeyResponse: + """ + Gets details for a specific key. + + Reference: + https://developers.deepgram.com/reference/get-key + """ + self._logger.debug("ManageClient.get_key ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/keys/{key_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("key_id: %s", key_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = KeyResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_key succeeded") + self._logger.debug("ManageClient.get_key LEAVE") + return res + + def create_key( + self, + project_id: str, + options: Union[Dict, KeyOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Key: + """ + Creates a new key. + + Reference: + https://developers.deepgram.com/reference/create-key + """ + self._logger.debug("ManageClient.create_key ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/keys" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, KeyOptions): + self._logger.info("KeyOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Key.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("create_key succeeded") + self._logger.debug("ManageClient.create_key LEAVE") + return res + + def delete_key( + self, + project_id: str, + key_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Deletes a key. + + Reference: + https://developers.deepgram.com/reference/delete-key + """ + self._logger.debug("ManageClient.delete_key ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/keys/{key_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("key_id: %s", key_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("delete_key succeeded") + self._logger.debug("ManageClient.delete_key LEAVE") + return res + + # members + def list_members( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> MembersResponse: + """ + Please see get_members for more information. + """ + return self.get_members( + project_id, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + def get_members( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> MembersResponse: + """ + Gets a list of members for a project. + + Reference: + https://developers.deepgram.com/reference/get-members + """ + self._logger.debug("ManageClient.get_members ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/members" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = MembersResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_members succeeded") + self._logger.debug("ManageClient.get_members LEAVE") + return res + + def remove_member( + self, + project_id: str, + member_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Removes a member from a project. + + Reference: + https://developers.deepgram.com/reference/remove-member + """ + self._logger.debug("ManageClient.remove_member ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/members/{member_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("member_id: %s", member_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("remove_member succeeded") + self._logger.debug("ManageClient.remove_member LEAVE") + return res + + # scopes + def get_member_scopes( + self, + project_id: str, + member_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> ScopesResponse: + """ + Gets a list of scopes for a member. + + Reference: + https://developers.deepgram.com/reference/get-member-scopes + """ + self._logger.debug("ManageClient.get_member_scopes ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/members/{member_id}/scopes" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("member_id: %s", member_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = ScopesResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_member_scopes succeeded") + self._logger.debug("ManageClient.get_member_scopes LEAVE") + return res + + def update_member_scope( + self, + project_id: str, + member_id: str, + options: Union[Dict, ScopeOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Updates a member's scopes. + + Reference: + https://developers.deepgram.com/reference/update-scope + """ + self._logger.debug("ManageClient.update_member_scope ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/members/{member_id}/scopes" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, ScopeOptions): + self._logger.info("ScopeOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.put( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("update_member_scope succeeded") + self._logger.debug("ManageClient.update_member_scope LEAVE") + return res + + # invites + def list_invites( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> InvitesResponse: + """ + Please see get_invites for more information. + """ + return self.get_invites( + project_id, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + def get_invites( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> InvitesResponse: + """ + Gets a list of invites for a project. + + Reference: + https://developers.deepgram.com/reference/list-invites + """ + self._logger.debug("ManageClient.get_invites ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/invites" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = InvitesResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_invites succeeded") + self._logger.debug("ManageClient.get_invites LEAVE") + return res + + def send_invite_options( + self, + project_id: str, + options: Union[Dict, InviteOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Sends an invite to a project. + + Reference: + https://developers.deepgram.com/reference/send-invite + """ + self._logger.debug("ManageClient.send_invite_options ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/invites" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, InviteOptions): + self._logger.info("InviteOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("send_invite_options succeeded") + self._logger.debug("ManageClient.send_invite_options LEAVE") + return res + + def send_invite( + self, + project_id: str, + email: str, + scope="member", + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Sends an invite to a project. + + Reference: + https://developers.deepgram.com/reference/send-invite + """ + self._logger.debug("ManageClient.send_invite ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/invites" + options = { + "email": email, + "scope": scope, + } + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, json=options, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("send_invite succeeded") + self._logger.debug("ManageClient.send_invite LEAVE") + return res + + def delete_invite( + self, + project_id: str, + email: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Deletes an invite from a project. + + Reference: + https://developers.deepgram.com/reference/delete-invite + """ + self._logger.debug("ManageClient.delete_invite ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/invites/{email}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("email: %s", email) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("delete_invite succeeded") + self._logger.debug("ManageClient.delete_invite LEAVE") + return res + + def leave_project( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Message: + """ + Leaves a project. + + Reference: + https://developers.deepgram.com/reference/leave-project + """ + self._logger.debug("ManageClient.leave_project ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/leave" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.delete( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Message.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("leave_project succeeded") + self._logger.debug("ManageClient.leave_project LEAVE") + return res + + # usage + def get_usage_requests( + self, + project_id: str, + options: Union[Dict, UsageRequestOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> UsageRequestsResponse: + """ + Gets a list of usage requests for a project. + + Reference: + https://developers.deepgram.com/reference/get-all-requests + """ + self._logger.debug("ManageClient.get_usage_requests ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/requests" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, UsageRequestOptions): + self._logger.info("UsageRequestOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, + options=options, + timeout=timeout, + addons=addons, + headers=headers, + **kwargs, + ) + self._logger.info("json: %s", result) + res = UsageRequestsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_usage_requests succeeded") + self._logger.debug("ManageClient.get_usage_requests LEAVE") + return res + + def get_usage_request( + self, + project_id: str, + request_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> UsageRequest: + """ + Gets details for a specific usage request. + + Reference: + https://developers.deepgram.com/reference/get-request + """ + self._logger.debug("ManageClient.get_usage_request ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/requests/{request_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("request_id: %s", request_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + + # convert str to JSON to check response field + json_result = json.loads(result) + if json_result.get("response") is None: + raise DeepgramError( + "Response is not available yet. Please try again later." + ) + + res = UsageRequest.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_usage_request succeeded") + self._logger.debug("ManageClient.get_usage_request LEAVE") + return res + + def get_usage_summary( + self, + project_id: str, + options: Union[Dict, UsageSummaryOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> UsageSummaryResponse: + """ + Gets a summary of usage for a project. + + Reference: + https://developers.deepgram.com/reference/summarize-usage + """ + self._logger.debug("ManageClient.get_usage_summary ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/usage" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, UsageSummaryOptions): + self._logger.info("UsageSummaryOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, + options=options, + timeout=timeout, + addons=addons, + headers=headers, + **kwargs, + ) + self._logger.info("json: %s", result) + res = UsageSummaryResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_usage_summary succeeded") + self._logger.debug("ManageClient.get_usage_summary LEAVE") + return res + + def get_usage_fields( + self, + project_id: str, + options: Union[Dict, UsageFieldsOptions], + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> UsageFieldsResponse: + """ + Gets a list of usage fields for a project. + + Reference: + https://developers.deepgram.com/reference/get-fields + """ + self._logger.debug("ManageClient.get_usage_fields ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/usage/fields" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + if isinstance(options, UsageFieldsOptions): + self._logger.info("UsageFieldsOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, + options=options, + timeout=timeout, + addons=addons, + headers=headers, + **kwargs, + ) + self._logger.info("json: %s", result) + res = UsageFieldsResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_usage_fields succeeded") + self._logger.debug("ManageClient.get_usage_fields LEAVE") + return res + + # balances + def list_balances( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> BalancesResponse: + """ + Please see get_balances for more information. + """ + return self.get_balances( + project_id, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + + def get_balances( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> BalancesResponse: + """ + Gets a list of balances for a project. + + Reference: + https://developers.deepgram.com/reference/get-all-balances + """ + self._logger.debug("ManageClient.get_balances ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/balances" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = BalancesResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_balances succeeded") + self._logger.debug("ManageClient.get_balances LEAVE") + return res + + def get_balance( + self, + project_id: str, + balance_id: str, + timeout: Optional[httpx.Timeout] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + **kwargs, + ) -> Balance: + """ + Gets details for a specific balance. + + Reference: + https://developers.deepgram.com/reference/get-balance + """ + self._logger.debug("ManageClient.get_balance ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/balances/{balance_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("balance_id: %s", balance_id) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.get( + url, timeout=timeout, addons=addons, headers=headers, **kwargs + ) + self._logger.info("json: %s", result) + res = Balance.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("get_balance succeeded") + self._logger.debug("ManageClient.get_balance LEAVE") + return res + + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/manage/v1/options.py b/deepgram/clients/manage/v1/options.py new file mode 100644 index 0000000000000000000000000000000000000000..34fbf87662c48bbda9f6a5f064b2e16a0983a68c --- /dev/null +++ b/deepgram/clients/manage/v1/options.py @@ -0,0 +1,177 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from ...common import BaseResponse + +# Input + + +@dataclass +class ProjectOptions(BaseResponse): + """ + Project Options + """ + + name: str = "" + + +@dataclass +class ModelOptions(BaseResponse): + """ + Model Options + """ + + include_outdated: bool = False + + +@dataclass +class KeyOptions(BaseResponse): + """ + Key Options + """ + + comment: Optional[str] = "" + expiration_date: Optional[str] = field( + default="", metadata=dataclass_config(exclude=lambda f: f == "") + ) + time_to_live_in_seconds: Optional[int] = field( + default=-1, metadata=dataclass_config(exclude=lambda f: f == -1) + ) + scopes: List[str] = field(default_factory=list) + tags: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if _dict["scopes"] is not None: + _dict["scopes"] = [str(scopes) for scopes in _dict["scopes"]] + if _dict["tags"] is not None: + _dict["tags"] = [str(tags) for tags in _dict["tags"]] + return _dict[key] + + +@dataclass +class ScopeOptions(BaseResponse): + """ + Scope Options + """ + + scope: str = "" + + +@dataclass +class InviteOptions(BaseResponse): + """ + Invite Options + """ + + email: str = "" + scope: str = "" + + +@dataclass +class UsageRequestOptions(BaseResponse): + """ + Usage Request Options + """ + + start: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + end: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + limit: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + status: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + +@dataclass +class UsageSummaryOptions(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + Usage Summary Options + """ + + start: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + end: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + accessor: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + tag: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + method: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + model: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + multichannel: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + interim_results: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + punctuate: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + ner: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + utterances: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + replace: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + profanity_filter: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + keywords: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + detect_topics: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + diarize: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + search: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + redact: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + alternatives: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + numerals: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + smart_format: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + +@dataclass +class UsageFieldsOptions(BaseResponse): + """ + Usage Fields Options + """ + + start: Optional[str] = "" + end: Optional[str] = "" diff --git a/deepgram/clients/manage/v1/response.py b/deepgram/clients/manage/v1/response.py new file mode 100644 index 0000000000000000000000000000000000000000..ef6824f4fd6973a93606353b024ccdcf2b4645c3 --- /dev/null +++ b/deepgram/clients/manage/v1/response.py @@ -0,0 +1,682 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import List, Optional, Dict, Any + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from ...common import ( + BaseResponse, +) + + +# Result Message + + +@dataclass +class Message(BaseResponse): + """ + Message from the Deepgram Platform + """ + + message: str = "" + + +# Projects + + +@dataclass +class Project(BaseResponse): + """ + Project object + """ + + project_id: str = "" + name: str = "" + + +@dataclass +class ProjectsResponse(BaseResponse): + """ + Projects Response object + """ + + projects: List[Project] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "projects" in _dict: + _dict["projects"] = [ + Project.from_dict(projects) for projects in _dict["projects"] + ] + return _dict[key] + + +# Models + + +@dataclass +class STTDetails(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + STTDetails class used to define the properties of the Speech-to-Text model response object. + """ + + name: str = "" + canonical_name: str = "" + architecture: str = "" + languages: List[str] = field(default_factory=list) + version: str = "" + uuid: str = "" + batch: bool = False + streaming: bool = False + formatted_output: bool = False + + def __getitem__(self, key): + _dict = self.to_dict() + if "languages" in _dict: + _dict["languages"] = [str(languages) for languages in _dict["languages"]] + return _dict[key] + + +@dataclass +class TTSMetadata(BaseResponse): + """ + TTSMetadata class used to define the properties for a given STT or TTS model. + """ + + accent: str = "" + color: str = "" + image: str = "" + sample: str = "" + tags: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + +@dataclass +class TTSDetails(BaseResponse): + """ + TTSDetails class used to define the properties of the Text-to-Speech model response object. + """ + + name: str = "" + canonical_name: str = "" + architecture: str = "" + languages: List[str] = field(default_factory=list) + version: str = "" + uuid: str = "" + metadata: Optional[TTSMetadata] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "metadata" in _dict: + _dict["metadata"] = [ + TTSMetadata.from_dict(metadata) for metadata in _dict["metadata"] + ] + return _dict[key] + + +# responses + + +@dataclass +class ModelResponse(BaseResponse): + """ + ModelResponse class used to define the properties of a single model. + """ + + name: str = "" + canonical_name: str = "" + architecture: str = "" + language: str = "" + version: str = "" + uuid: str = "" + metadata: Optional[TTSMetadata] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "metadata" in _dict: + _dict["metadata"] = [ + TTSMetadata.from_dict(metadata) for metadata in _dict["metadata"] + ] + return _dict[key] + + +@dataclass +class ModelsResponse(BaseResponse): + """ + ModelsResponse class used to obtain a list of models. + """ + + stt: List[STTDetails] = field(default_factory=list) + tts: List[TTSDetails] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "stt" in _dict: + _dict["stt"] = [STTDetails.from_dict(stt) for stt in _dict["stt"]] + if "tts" in _dict: + _dict["tts"] = [TTSDetails.from_dict(tts) for tts in _dict["tts"]] + return _dict[key] + + +# Members + + +@dataclass +class Member(BaseResponse): + """ + Member object + """ + + email: str = "" + first_name: str = "" + last_name: str = "" + member_id: str = "" + + +@dataclass +class MembersResponse(BaseResponse): + """ + Members Response object + """ + + members: List[Member] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "members" in _dict: + _dict["members"] = [ + Member.from_dict(members) for members in _dict["members"] + ] + return _dict[key] + + +# Keys + + +@dataclass +class Key(BaseResponse): + """ + Key object + """ + + api_key_id: str = "" + key: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + comment: Optional[str] = "" + created: str = "" + scopes: List[str] = field(default_factory=list) + expiration_date: str = field( + default="", metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "scopes" in _dict: + _dict["scopes"] = [str(scopes) for scopes in _dict["scopes"]] + return _dict[key] + + +@dataclass +class KeyResponse(BaseResponse): + """ + Key Response object + """ + + api_key: Key + member: Member + + def __getitem__(self, key): + _dict = self.to_dict() + if "api_key" in _dict: + _dict["api_key"] = Key.from_dict(_dict["api_key"]) + if "member" in _dict: + _dict["member"] = Member.from_dict(_dict["member"]) + return _dict[key] + + +@dataclass +class KeysResponse(BaseResponse): + """ + Keys Response object + """ + + api_keys: List[KeyResponse] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "api_keys" in _dict: + _dict["api_keys"] = [ + KeyResponse.from_dict(api_keys) for api_keys in _dict["api_keys"] + ] + return _dict[key] + + +# Scopes + + +@dataclass +class ScopesResponse(BaseResponse): + """ + Scopes Response object + """ + + scopes: List[str] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "scopes" in _dict: + _dict["scopes"] = [str(scopes) for scopes in _dict["scopes"]] + return _dict[key] + + +# Invites + + +@dataclass +class Invite(BaseResponse): + """ + Invite object + """ + + email: str = "" + scope: str = "" + + +@dataclass +class InvitesResponse(BaseResponse): + """ + Invites Response object + """ + + invites: List[Invite] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "invites" in _dict: + _dict["invites"] = [ + Invite.from_dict(invites) for invites in _dict["invites"] + ] + return _dict[key] + + +# Usage + + +@dataclass +class Config(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + Config object + """ + + language: str = "" + model: str = "" + punctuate: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + utterances: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + diarize: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + smart_format: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + interim_results: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + topics: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + intents: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sentiment: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + summarize: Optional[bool] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + +@dataclass +class STTUsageDetails(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + Details object + """ + + config: Config + usd: float = 0 + duration: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + total_audio: Optional[float] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + channels: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + streams: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + method: str = "" + tier: Optional[str] = "" + models: List[str] = field(default_factory=list) + tags: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + features: List[str] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "models" in _dict: + _dict["models"] = [str(models) for models in _dict["models"]] + if "tags" in _dict: + _dict["tags"] = [str(tags) for tags in _dict["tags"]] + if "features" in _dict: + _dict["features"] = [str(features) for features in _dict["features"]] + if "config" in _dict: + _dict["config"] = Config.from_dict(_dict["config"]) + return _dict[key] + + +@dataclass +class Callback(BaseResponse): + """ + Callback object + """ + + attempts: int = 0 + code: int = 0 + completed: str = "" + + +@dataclass +class TokenDetail(BaseResponse): + """ + Token Detail object + """ + + feature: str = "" + input: int = 0 + model: str = "" + output: int = 0 + + +@dataclass +class SpeechSegment(BaseResponse): + """ + Speech Segment object + """ + + characters: int = 0 + model: str = "" + tier: str = "" + + +@dataclass +class TTSUsageDetails(BaseResponse): + """ + TTS Details object + """ + + duration: float = 0 + speech_segments: List[SpeechSegment] = field(default_factory=list) + # pylint: disable=fixme + # TODO: audio_metadata: None + # pylint: enable=fixme + + def __getitem__(self, key): + _dict = self.to_dict() + if "speech_segments" in _dict: + _dict["speech_segments"] = [ + SpeechSegment.from_dict(speech_segments) + for speech_segments in _dict["speech_segments"] + ] + return _dict[key] + + +@dataclass +class UsageResponse(BaseResponse): + """ + UsageResponse object + """ + + details: Optional[STTUsageDetails] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + code: int = 0 + completed: str = "" + message: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + tts_details: Optional[TTSUsageDetails] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + token_details: List[TokenDetail] = field( + default_factory=list, metadata=dataclass_config(exclude=lambda f: f is list) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "details" in _dict: + _dict["details"] = STTUsageDetails.from_dict(_dict["details"]) + if "tts_details" in _dict: + _dict["tts_details"] = TTSUsageDetails.from_dict(_dict["tts_details"]) + if "token_details" in _dict: + _dict["token_details"] = [ + TokenDetail.from_dict(token_details) + for token_details in _dict["token_details"] + ] + return _dict[key] + + +@dataclass +class UsageRequest(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + Usage Request object + """ + + response: UsageResponse + project_uuid: str = "" + request_id: str = "" + created: str = "" + path: str = "" + api_key_id: str = "" + callback: Optional[Callback] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + accessor: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "response" in _dict: + _dict["response"] = UsageResponse.from_dict(_dict["response"]) + if "callback" in _dict: + _dict["callback"] = Callback.from_dict(_dict["callback"]) + return _dict[key] + + +@dataclass +class UsageRequestsResponse(BaseResponse): + """ + Usage Requests Response object + """ + + page: int = 0 + limit: int = 0 + requests: List[UsageRequest] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "requests" in _dict: + _dict["requests"] = [ + UsageRequest.from_dict(requests) for requests in _dict["requests"] + ] + return _dict[key] + + +@dataclass +class STTTokens(BaseResponse): + """ + STTTokens object + """ + + tokens_in: int = 0 + out: int = 0 + + +@dataclass +class TTSTokens(BaseResponse): + """ + TTSTokens object + """ + + characters: int = 0 + requests: int = 0 + + +@dataclass +class UsageSummaryResults(BaseResponse): + """ + Results object + """ + + tokens: STTTokens + tts: TTSTokens + start: str = "" + end: str = "" + hours: int = 0 + total_hours: int = 0 + requests: int = 0 + + def __getitem__(self, key): + _dict = self.to_dict() + if "tokens" in _dict: + _dict["tokens"] = STTTokens.from_dict(_dict["tokens"]) + if "tts" in _dict: + _dict["tts"] = TTSTokens.from_dict(_dict["tts"]) + return _dict[key] + + +@dataclass +class Resolution(BaseResponse): + """ + Resolution object + """ + + units: str = "" + amount: int = 0 + + +@dataclass +class UsageSummaryResponse(BaseResponse): + """ + Usage Summary Response object + """ + + resolution: Resolution + start: str = "" + end: str = "" + results: List[UsageSummaryResults] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "resolution" in _dict: + _dict["resolution"] = Resolution.from_dict(_dict["resolution"]) + if "results" in _dict: + _dict["results"] = [ + UsageSummaryResults.from_dict(results) for results in _dict["results"] + ] + return _dict[key] + + +@dataclass +class UsageModel(BaseResponse): + """ + Usage Model object + """ + + name: str = "" + language: str = "" + version: str = "" + model_id: str = "" + + +@dataclass +class UsageFieldsResponse(BaseResponse): + """ + Usage Fields Response object + """ + + tags: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + models: List[UsageModel] = field(default_factory=list) + processing_methods: List[str] = field(default_factory=list) + features: List[str] = field(default_factory=list) + languages: Optional[List[str]] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + if "tags" in _dict: + _dict["tags"] = [str(tags) for tags in _dict["tags"]] + if "models" in _dict: + _dict["models"] = [ + UsageModel.from_dict(models) for models in _dict["models"] + ] + if "processing_methods" in _dict: + _dict["processing_methods"] = [ + str(processing_methods) + for processing_methods in _dict["processing_methods"] + ] + if "features" in _dict: + _dict["features"] = [str(features) for features in _dict["features"]] + if "languages" in _dict: + _dict["languages"] = [str(languages) for languages in _dict["languages"]] + return _dict[key] + + +# Billing + + +@dataclass +class Balance(BaseResponse): + """ + Balance object + """ + + balance_id: str = "" + amount: str = "" + units: str = "" + purchase_order_id: str = "" + + +@dataclass +class BalancesResponse(BaseResponse): + """ + Balances Response object + """ + + balances: List[Balance] = field(default_factory=list) + + def __getitem__(self, key): + _dict = self.to_dict() + if "balances" in _dict: + _dict["balances"] = [ + Balance.from_dict(balances) for balances in _dict["balances"] + ] + return _dict[key] diff --git a/deepgram/clients/prerecorded/__init__.py b/deepgram/clients/prerecorded/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9156a65c3f0c876166e7fb9333e4993a76d268a1 --- /dev/null +++ b/deepgram/clients/prerecorded/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .v1 import PreRecordedClient +from .v1 import AsyncPreRecordedClient +from .v1 import PrerecordedOptions +from .v1 import ( + UrlSource, + FileSource, + PreRecordedStreamSource, + PrerecordedSource, +) +from .v1 import ( + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, +) diff --git a/deepgram/clients/prerecorded/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/prerecorded/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..157441d29a5de16297d8435a14801ce76ba16a99 Binary files /dev/null and b/deepgram/clients/prerecorded/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/prerecorded/v1/__init__.py b/deepgram/clients/prerecorded/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..03095cc97c6febcbddd054a5770ab7aa089d98ff --- /dev/null +++ b/deepgram/clients/prerecorded/v1/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import PreRecordedClient +from .client import AsyncPreRecordedClient +from .client import PrerecordedOptions +from .client import ( + UrlSource, + FileSource, + PreRecordedStreamSource, + PrerecordedSource, +) +from .client import ( + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, +) diff --git a/deepgram/clients/prerecorded/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/prerecorded/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..973cb06d1474f58f6a25d0d1fe2ec29d62bceea5 Binary files /dev/null and b/deepgram/clients/prerecorded/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/prerecorded/v1/__pycache__/client.cpython-313.pyc b/deepgram/clients/prerecorded/v1/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9ed5d2c34c8e98f8f53940f80ed83892d63bc4c Binary files /dev/null and b/deepgram/clients/prerecorded/v1/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/prerecorded/v1/client.py b/deepgram/clients/prerecorded/v1/client.py new file mode 100644 index 0000000000000000000000000000000000000000..0a8819d3b0f49e24d9ded8164d2e882a76ece8e0 --- /dev/null +++ b/deepgram/clients/prerecorded/v1/client.py @@ -0,0 +1,37 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from ...listen import PreRecordedClient as PreRecordedClientLatest +from ...listen import AsyncPreRecordedClient as AsyncPreRecordedClientLatest +from ...listen import ( + PrerecordedOptions as PrerecordedOptionsLatest, + UrlSource as UrlSourceLatest, + FileSource as FileSourceLatest, + PreRecordedStreamSource as PreRecordedStreamSourceLatest, + PrerecordedSource as PrerecordedSourceLatest, +) +from ...listen import ( + AsyncPrerecordedResponse as AsyncPrerecordedResponseLatest, + PrerecordedResponse as PrerecordedResponseLatest, + SyncPrerecordedResponse as SyncPrerecordedResponseLatest, +) + + +# input +PrerecordedOptions = PrerecordedOptionsLatest +PreRecordedStreamSource = PreRecordedStreamSourceLatest +UrlSource = UrlSourceLatest +FileSource = FileSourceLatest +PrerecordedSource = PrerecordedSourceLatest + + +# output +AsyncPrerecordedResponse = AsyncPrerecordedResponseLatest +PrerecordedResponse = PrerecordedResponseLatest +SyncPrerecordedResponse = SyncPrerecordedResponseLatest + + +# clients +PreRecordedClient = PreRecordedClientLatest +AsyncPreRecordedClient = AsyncPreRecordedClientLatest diff --git a/deepgram/clients/prerecorded/v1/errors.py b/deepgram/clients/prerecorded/v1/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..41a6947c564bb0f1b03a65a0ef068345d1977754 --- /dev/null +++ b/deepgram/clients/prerecorded/v1/errors.py @@ -0,0 +1,37 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + + +class DeepgramError(Exception): + """ + Exception raised for unknown errors related to the Deepgram API. + + Attributes: + message (str): The error message describing the exception. + """ + + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" + + +class DeepgramTypeError(Exception): + """ + Exception raised for unknown errors related to unknown Types for Transcription. + + Attributes: + message (str): The error message describing the exception. + """ + + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramTypeError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" diff --git a/deepgram/clients/read_router.py b/deepgram/clients/read_router.py new file mode 100644 index 0000000000000000000000000000000000000000..1ca09ee3f67f0ca0f49872b75191b6f148f8bca3 --- /dev/null +++ b/deepgram/clients/read_router.py @@ -0,0 +1,130 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from importlib import import_module +import logging + +from ..utils import verboselogs +from ..options import DeepgramClientOptions +from .errors import DeepgramModuleError + + +class ReadRouter: + """ + Represents a client for interacting with the Deepgram API. + + This class provides a client for making requests to the Deepgram API with various configuration options. + + Attributes: + config_options (DeepgramClientOptions): An optional configuration object specifying client options. + + Raises: + DeepgramApiKeyError: If the API key is missing or invalid. + + Methods: + read: (Preferred) Returns an Threaded AnalyzeClient instance for interacting with Deepgram's read transcription services. + asyncread: Returns an (Async) AnalyzeClient instance for interacting with Deepgram's read transcription services. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + + @property + def analyze(self): + """ + Returns an AnalyzeClient instance for interacting with Deepgram's read services. + """ + return self.Version(self._config, "analyze") + + @property + def asyncanalyze(self): + """ + Returns an AsyncAnalyzeClient instance for interacting with Deepgram's read services. + """ + return self.Version(self._config, "asyncanalyze") + + # INTERNAL CLASSES + class Version: + """ + Represents a version of the Deepgram API. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _parent: str + + def __init__(self, config, parent: str): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._parent = parent + + # FUTURE VERSIONING: + # When v2 or v1.1beta1 or etc. This allows easy access to the latest version of the API. + # @property + # def latest(self): + # match self._parent: + # case "analyze": + # return AnalyzeClient(self._config) + # case _: + # raise DeepgramModuleError("Invalid parent") + + def v(self, version: str = ""): + """ + Returns a specific version of the Deepgram API. + """ + self._logger.debug("Version.v ENTER") + self._logger.info("version: %s", version) + if len(version) == 0: + self._logger.error("version is empty") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid module version") + + parent = "" + file_name = "" + class_name = "" + match self._parent: + case "analyze": + parent = "analyze" + file_name = "client" + class_name = "AnalyzeClient" + case "asyncanalyze": + parent = "analyze" + file_name = "async_client" + class_name = "AsyncAnalyzeClient" + case _: + self._logger.error("parent unknown: %s", self._parent) + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid parent type") + + # create class path + path = f"deepgram.clients.{parent}.v{version}.{file_name}" + self._logger.info("path: %s", path) + self._logger.info("class_name: %s", class_name) + + # import class + mod = import_module(path) + if mod is None: + self._logger.error("module path is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find package") + + my_class = getattr(mod, class_name) + if my_class is None: + self._logger.error("my_class is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find class") + + # instantiate class + my_class = my_class(self._config) + self._logger.notice("Version.v succeeded") + self._logger.debug("Version.v LEAVE") + return my_class diff --git a/deepgram/clients/selfhosted/__init__.py b/deepgram/clients/selfhosted/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ced8a6f400722774cfc629d357544daabc7d84cd --- /dev/null +++ b/deepgram/clients/selfhosted/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import SelfHostedClient, OnPremClient +from .client import AsyncSelfHostedClient, AsyncOnPremClient diff --git a/deepgram/clients/selfhosted/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/selfhosted/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f03048d1a314ed905953e73b5294f340bbe0c6b3 Binary files /dev/null and b/deepgram/clients/selfhosted/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/selfhosted/__pycache__/client.cpython-313.pyc b/deepgram/clients/selfhosted/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a11feac9d66ff85998126600fd831227ffbe6f5d Binary files /dev/null and b/deepgram/clients/selfhosted/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/selfhosted/client.py b/deepgram/clients/selfhosted/client.py new file mode 100644 index 0000000000000000000000000000000000000000..1301c641a27283c4e783a67aebdb8aa66cc10641 --- /dev/null +++ b/deepgram/clients/selfhosted/client.py @@ -0,0 +1,15 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .v1.client import SelfHostedClient as SelfHostedClientLatest +from .v1.async_client import AsyncSelfHostedClient as AsyncSelfHostedClientLatest + + +# The client.py points to the current supported version in the SDK. +# Older versions are supported in the SDK for backwards compatibility. + +SelfHostedClient = SelfHostedClientLatest +AsyncSelfHostedClient = AsyncSelfHostedClientLatest +OnPremClient = SelfHostedClientLatest +AsyncOnPremClient = AsyncSelfHostedClientLatest diff --git a/deepgram/clients/selfhosted/v1/__init__.py b/deepgram/clients/selfhosted/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8604f754019c17c9c60a7fc157f9616a95a7c6b2 --- /dev/null +++ b/deepgram/clients/selfhosted/v1/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import SelfHostedClient +from .async_client import AsyncSelfHostedClient diff --git a/deepgram/clients/selfhosted/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/selfhosted/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30e5f1e2e74a96a3872261f863390045437fba23 Binary files /dev/null and b/deepgram/clients/selfhosted/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/selfhosted/v1/__pycache__/async_client.cpython-313.pyc b/deepgram/clients/selfhosted/v1/__pycache__/async_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34188c61a939731f754091efd0a1dbbac6d32b0e Binary files /dev/null and b/deepgram/clients/selfhosted/v1/__pycache__/async_client.cpython-313.pyc differ diff --git a/deepgram/clients/selfhosted/v1/__pycache__/client.cpython-313.pyc b/deepgram/clients/selfhosted/v1/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07448d7b60c96e067ecbae2d23a981a2e56e86de Binary files /dev/null and b/deepgram/clients/selfhosted/v1/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/selfhosted/v1/async_client.py b/deepgram/clients/selfhosted/v1/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..cc9bb1757b21fa4eb54e08f711712b3a88a96021 --- /dev/null +++ b/deepgram/clients/selfhosted/v1/async_client.py @@ -0,0 +1,170 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Optional + +import httpx + +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from ...common import AbstractAsyncRestClient + + +class AsyncSelfHostedClient(AbstractAsyncRestClient): + """ + Client for interacting with Deepgram's on-premises API. + + This class provides methods to manage and interact with on-premises projects and distribution credentials. + + Args: + config (DeepgramClientOptions): all the options for the client. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._endpoint = "v1/projects" + super().__init__(config) + + async def list_onprem_credentials( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + List all on-premises distribution credentials for a project. + """ + return self.list_selfhosted_credentials(project_id, timeout=timeout, **kwargs) + + async def list_selfhosted_credentials( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + List all on-premises distribution credentials for a project. + """ + self._logger.debug("SelfHostedClient.list_selfhosted_credentials ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/selfhosted/distribution/credentials" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + res = await self.get(url, timeout=timeout, **kwargs) + self._logger.verbose("result: %s", res) + self._logger.notice("list_selfhosted_credentials succeeded") + self._logger.debug("SelfHostedClient.list_selfhosted_credentials LEAVE") + return res + + async def get_onprem_credentials( + self, + project_id: str, + distribution_credentials_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Get a specific on-premises distribution credential for a project. + """ + return self.get_selfhosted_credentials( + project_id, distribution_credentials_id, timeout=timeout, **kwargs + ) + + async def get_selfhosted_credentials( + self, + project_id: str, + distribution_credentials_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Get a specific on-premises distribution credential for a project. + """ + self._logger.debug("SelfHostedClient.get_selfhosted_credentials ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/selfhosted/distribution/credentials/{distribution_credentials_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info( + "distribution_credentials_id: %s", distribution_credentials_id + ) + res = await self.get(url, timeout=timeout, **kwargs) + self._logger.verbose("result: %s", res) + self._logger.notice("get_selfhosted_credentials succeeded") + self._logger.debug("SelfHostedClient.get_selfhosted_credentials LEAVE") + return res + + async def create_onprem_credentials( + self, + project_id: str, + options, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Create a new on-premises distribution credential for a project. + """ + return self.create_onprem_credentials(project_id, options, timeout, **kwargs) + + async def create_selfhosted_credentials( + self, + project_id: str, + options, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Create a new on-premises distribution credential for a project. + """ + self._logger.debug("SelfHostedClient.create_selfhosted_credentials ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/selfhosted/distribution/credentials/" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("options: %s", options) + res = await self.post(url, json=options, timeout=timeout, **kwargs) + self._logger.verbose("result: %s", res) + self._logger.notice("create_selfhosted_credentials succeeded") + self._logger.debug("SelfHostedClient.create_selfhosted_credentials LEAVE") + return res + + async def delete_onprem_credentials( + self, + project_id: str, + distribution_credentials_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Delete an on-premises distribution credential for a project. + """ + return self.delete_selfhosted_credentials( + project_id, distribution_credentials_id, timeout=timeout, **kwargs + ) + + async def delete_selfhosted_credentials( + self, + project_id: str, + distribution_credentials_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Delete an on-premises distribution credential for a project. + """ + self._logger.debug("SelfHostedClient.delete_selfhosted_credentials ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/selfhosted/distribution/credentials/{distribution_credentials_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("distrbution_credentials_id: %s", distribution_credentials_id) + res = await self.delete(url, timeout=timeout, **kwargs) + self._logger.verbose("result: %s", res) + self._logger.notice("delete_selfhosted_credentials succeeded") + self._logger.debug("SelfHostedClient.delete_selfhosted_credentials LEAVE") + return res diff --git a/deepgram/clients/selfhosted/v1/client.py b/deepgram/clients/selfhosted/v1/client.py new file mode 100644 index 0000000000000000000000000000000000000000..86d63ff242bb5ce95a9bacdbfcecb6ae164e31f7 --- /dev/null +++ b/deepgram/clients/selfhosted/v1/client.py @@ -0,0 +1,172 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Optional + +import httpx + +from ....utils import verboselogs +from ....options import DeepgramClientOptions +from ...common import AbstractSyncRestClient + + +class SelfHostedClient(AbstractSyncRestClient): + """ + Client for interacting with Deepgram's on-premises API. + + This class provides methods to manage and interact with on-premises projects and distribution credentials. + + Args: + config (DeepgramClientOptions): all the options for the client. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._endpoint = "v1/projects" + super().__init__(config) + + def list_onprem_credentials( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + List all on-premises distribution credentials for a project. + """ + return self.list_selfhosted_credentials(project_id, timeout=timeout, **kwargs) + + def list_selfhosted_credentials( + self, + project_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + List all on-premises distribution credentials for a project. + """ + self._logger.debug("SelfHostedClient.list_selfhosted_credentials ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/selfhosted/distribution/credentials" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + res = self.get(url, timeout=timeout, **kwargs) + self._logger.verbose("result: %s", res) + self._logger.notice("list_selfhosted_credentials succeeded") + self._logger.debug("SelfHostedClient.list_selfhosted_credentials LEAVE") + return res + + def get_onprem_credentials( + self, + project_id: str, + distribution_credentials_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Get a specific on-premises distribution credential for a project. + """ + return self.get_selfhosted_credentials( + project_id, distribution_credentials_id, timeout=timeout, **kwargs + ) + + def get_selfhosted_credentials( + self, + project_id: str, + distribution_credentials_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Get a specific on-premises distribution credential for a project. + """ + self._logger.debug("SelfHostedClient.get_selfhosted_credentials ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/selfhosted/distribution/credentials/{distribution_credentials_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info( + "distribution_credentials_id: %s", distribution_credentials_id + ) + res = self.get(url, timeout=timeout, **kwargs) + self._logger.verbose("result: %s", res) + self._logger.notice("get_selfhosted_credentials succeeded") + self._logger.debug("SelfHostedClient.get_selfhosted_credentials LEAVE") + return res + + def create_onprem_credentials( + self, + project_id: str, + options, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Create a new on-premises distribution credential for a project. + """ + return self.create_selfhosted_credentials( + project_id, options, timeout=timeout, **kwargs + ) + + def create_selfhosted_credentials( + self, + project_id: str, + options, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Create a new on-premises distribution credential for a project. + """ + self._logger.debug("SelfHostedClient.create_selfhosted_credentials ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/selfhosted/distribution/credentials/" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("options: %s", options) + res = self.post(url, json=options, timeout=timeout, **kwargs) + self._logger.verbose("result: %s", res) + self._logger.notice("create_selfhosted_credentials succeeded") + self._logger.debug("SelfHostedClient.create_selfhosted_credentials LEAVE") + return res + + def delete_onprem_credentials( + self, + project_id: str, + distribution_credentials_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Delete an on-premises distribution credential for a project. + """ + return self.delete_selfhosted_credentials( + project_id, distribution_credentials_id, timeout=timeout, **kwargs + ) + + def delete_selfhosted_credentials( + self, + project_id: str, + distribution_credentials_id: str, + timeout: Optional[httpx.Timeout] = None, + **kwargs, + ): + """ + Delete an on-premises distribution credential for a project. + """ + self._logger.debug("SelfHostedClient.delete_selfhosted_credentials ENTER") + url = f"{self._config.url}/{self._endpoint}/{project_id}/selfhosted/distribution/credentials/{distribution_credentials_id}" + self._logger.info("url: %s", url) + self._logger.info("project_id: %s", project_id) + self._logger.info("distrbution_credentials_id: %s", distribution_credentials_id) + res = self.delete(url, timeout=timeout, **kwargs) + self._logger.verbose("result: %s", res) + self._logger.notice("delete_selfhosted_credentials succeeded") + self._logger.debug("SelfHostedClient.delete_selfhosted_credentials LEAVE") + return res diff --git a/deepgram/clients/speak/__init__.py b/deepgram/clients/speak/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..874a173988321fd0600e32acc72710df5f9ee941 --- /dev/null +++ b/deepgram/clients/speak/__init__.py @@ -0,0 +1,53 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .enums import SpeakWebSocketEvents, SpeakWebSocketMessage + +# rest +from .client import ( + SpeakClient, # backward compat + SpeakRESTClient, + AsyncSpeakRESTClient, +) +from .client import ( + #### top level + SpeakRESTOptions, + SpeakOptions, + # common + TextSource, + BufferSource, + StreamSource, + FileSource, + # unique + SpeakSource, + SpeakRestSource, + SpeakRESTSource, +) +from .client import ( + SpeakResponse, # backward compat + SpeakRESTResponse, +) + +# websocket +from .client import ( + SpeakWSOptions, +) +from .client import ( + SpeakWebSocketClient, + AsyncSpeakWebSocketClient, + SpeakWSClient, + AsyncSpeakWSClient, +) +from .client import ( + #### top level + SpeakWSMetadataResponse, + FlushedResponse, + ClearedResponse, + WarningResponse, + #### shared + OpenResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) diff --git a/deepgram/clients/speak/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/speak/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0566db65be29ab433fe0cacac35a778c27d16337 Binary files /dev/null and b/deepgram/clients/speak/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/speak/__pycache__/client.cpython-313.pyc b/deepgram/clients/speak/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60131df232fa800b80f9f28ea637db1a8c7acff7 Binary files /dev/null and b/deepgram/clients/speak/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/speak/__pycache__/enums.cpython-313.pyc b/deepgram/clients/speak/__pycache__/enums.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc0204ca158c29df8d0e367a228943e3f526dbbf Binary files /dev/null and b/deepgram/clients/speak/__pycache__/enums.cpython-313.pyc differ diff --git a/deepgram/clients/speak/client.py b/deepgram/clients/speak/client.py new file mode 100644 index 0000000000000000000000000000000000000000..855555c64ae12c11996213f84e8b973d63f9eafd --- /dev/null +++ b/deepgram/clients/speak/client.py @@ -0,0 +1,95 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +# rest +from .v1 import ( + #### top level + SpeakOptions as SpeakOptionsLatest, + SpeakRESTOptions as SpeakRESTOptionsLatest, + # common + TextSource as TextSourceLatest, + BufferSource as BufferSourceLatest, + StreamSource as StreamSourceLatest, + FileSource as FileSourceLatest, + # unique + SpeakSource as SpeakSourceLatest, + SpeakRestSource as SpeakRestSourceLatest, + SpeakRESTSource as SpeakRESTSourceLatest, +) + +from .v1 import ( + SpeakRESTClient as SpeakRESTClientLatest, + AsyncSpeakRESTClient as AsyncSpeakRESTClientLatest, +) + +from .v1 import ( + SpeakRESTResponse as SpeakRESTResponseLatest, +) + +# websocket +from .v1 import ( + SpeakWebSocketClient as SpeakWebSocketClientLatest, + AsyncSpeakWebSocketClient as AsyncSpeakWebSocketClientLatest, + SpeakWSClient as SpeakWSClientLatest, + AsyncSpeakWSClient as AsyncSpeakWSClientLatest, +) + +from .v1 import ( + SpeakWSOptions as SpeakWSOptionsLatest, +) +from .v1 import ( + OpenResponse as OpenResponseLatest, + SpeakWSMetadataResponse as SpeakWSMetadataResponseLatest, + FlushedResponse as FlushedResponseLatest, + ClearedResponse as ClearedResponseLatest, + CloseResponse as CloseResponseLatest, + UnhandledResponse as UnhandledResponseLatest, + WarningResponse as WarningResponseLatest, + ErrorResponse as ErrorResponseLatest, +) + +# The client.py points to the current supported version in the SDK. +# Older versions are supported in the SDK for backwards compatibility. + +# rest +# input +SpeakOptions = SpeakOptionsLatest +SpeakRESTOptions = SpeakRESTOptionsLatest +TextSource = TextSourceLatest +BufferSource = BufferSourceLatest +StreamSource = StreamSourceLatest +FileSource = FileSourceLatest +SpeakSource = SpeakSourceLatest +SpeakRestSource = SpeakRestSourceLatest +SpeakRESTSource = SpeakRESTSourceLatest # pylint: disable=invalid-name + +# output +SpeakRESTResponse = SpeakRESTResponseLatest + +# websocket +# input +SpeakWSOptions = SpeakWSOptionsLatest + +# output +OpenResponse = OpenResponseLatest +SpeakWSMetadataResponse = SpeakWSMetadataResponseLatest +FlushedResponse = FlushedResponseLatest +ClearedResponse = ClearedResponseLatest +CloseResponse = CloseResponseLatest +UnhandledResponse = UnhandledResponseLatest +WarningResponse = WarningResponseLatest +ErrorResponse = ErrorResponseLatest + + +# backward compatibility +SpeakResponse = SpeakRESTResponseLatest +SpeakClient = SpeakRESTClientLatest + +# clients +SpeakRESTClient = SpeakRESTClientLatest +AsyncSpeakRESTClient = AsyncSpeakRESTClientLatest +SpeakWSClient = SpeakWSClientLatest +AsyncSpeakWSClient = AsyncSpeakWSClientLatest +SpeakWebSocketClient = SpeakWebSocketClientLatest +AsyncSpeakWebSocketClient = AsyncSpeakWebSocketClientLatest diff --git a/deepgram/clients/speak/enums.py b/deepgram/clients/speak/enums.py new file mode 100644 index 0000000000000000000000000000000000000000..a9d2a5a79b897e1bc6ef774bd825de1e5c875167 --- /dev/null +++ b/deepgram/clients/speak/enums.py @@ -0,0 +1,34 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from aenum import StrEnum + +# Constants mapping to events from the Deepgram API + + +class SpeakWebSocketMessage(StrEnum): + """ + Enumerates the possible message types that can be received from the Deepgram API + """ + + Speak: str = "Speak" + Flush: str = "Flush" + Clear: str = "Clear" + Close: str = "Close" + + +class SpeakWebSocketEvents(StrEnum): + """ + Enumerates the possible events that can be received from the Deepgram API + """ + + Open: str = "Open" + Close: str = "Close" + AudioData: str = "AudioData" + Metadata: str = "Metadata" + Flushed: str = "Flushed" + Cleared: str = "Cleared" + Unhandled: str = "Unhandled" + Error: str = "Error" + Warning: str = "Warning" diff --git a/deepgram/clients/speak/v1/__init__.py b/deepgram/clients/speak/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d878cc5af9094eaa93e94ad30c8da2b5aa1a26c0 --- /dev/null +++ b/deepgram/clients/speak/v1/__init__.py @@ -0,0 +1,48 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +# rest +from .rest import ( + #### top level + SpeakRESTOptions, + SpeakOptions, + # common + TextSource, + BufferSource, + StreamSource, + FileSource, + # unique + SpeakSource, + SpeakRestSource, + SpeakRESTSource, +) +from .rest import ( + SpeakRESTOptions, + SpeakOptions, +) +from .rest import SpeakRESTClient, AsyncSpeakRESTClient +from .rest import SpeakRESTResponse + +# websocket +from .websocket import ( + SpeakWSOptions, +) +from .websocket import ( + SpeakWebSocketClient, + AsyncSpeakWebSocketClient, + SpeakWSClient, + AsyncSpeakWSClient, +) +from .websocket import ( + #### top level + MetadataResponse as SpeakWSMetadataResponse, + FlushedResponse, + ClearedResponse, + WarningResponse, + #### shared + OpenResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) diff --git a/deepgram/clients/speak/v1/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/speak/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1876f9a9fbd4743b8d3a33b735cd42c34efc6764 Binary files /dev/null and b/deepgram/clients/speak/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/rest/__init__.py b/deepgram/clients/speak/v1/rest/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..084cef64371d68b09ad0539f0a6ad7aa819b2dbf --- /dev/null +++ b/deepgram/clients/speak/v1/rest/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import SpeakRESTClient +from .async_client import AsyncSpeakRESTClient +from .response import SpeakRESTResponse +from .options import ( + #### top level + SpeakRESTOptions, + SpeakOptions, + # common + TextSource, + BufferSource, + StreamSource, + FileSource, + # unique + SpeakSource, + SpeakRestSource, + SpeakRESTSource, +) diff --git a/deepgram/clients/speak/v1/rest/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/speak/v1/rest/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fecb64a46b5b2e3cfa574a7d9acf879a83903fa5 Binary files /dev/null and b/deepgram/clients/speak/v1/rest/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/rest/__pycache__/async_client.cpython-313.pyc b/deepgram/clients/speak/v1/rest/__pycache__/async_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d744c6b3ca5ccc92f761c2fb6c9f5ad756e10459 Binary files /dev/null and b/deepgram/clients/speak/v1/rest/__pycache__/async_client.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/rest/__pycache__/client.cpython-313.pyc b/deepgram/clients/speak/v1/rest/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f54cb49f6ec91bdf5a21016730985d1e1c672364 Binary files /dev/null and b/deepgram/clients/speak/v1/rest/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/rest/__pycache__/helpers.cpython-313.pyc b/deepgram/clients/speak/v1/rest/__pycache__/helpers.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bcca5cee4a9f3e663e78c257637470bae279e13a Binary files /dev/null and b/deepgram/clients/speak/v1/rest/__pycache__/helpers.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/rest/__pycache__/options.cpython-313.pyc b/deepgram/clients/speak/v1/rest/__pycache__/options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56eeb604741fa17de47f6427b52b4f7f04738f88 Binary files /dev/null and b/deepgram/clients/speak/v1/rest/__pycache__/options.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/rest/__pycache__/response.cpython-313.pyc b/deepgram/clients/speak/v1/rest/__pycache__/response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..347e7bab786289f6b2ad08d3e64bf2facbf84038 Binary files /dev/null and b/deepgram/clients/speak/v1/rest/__pycache__/response.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/rest/async_client.py b/deepgram/clients/speak/v1/rest/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..fc83926b51497d2b3af2ecf29ae99fefe76e3181 --- /dev/null +++ b/deepgram/clients/speak/v1/rest/async_client.py @@ -0,0 +1,309 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Dict, Union, Optional, cast +import io +import aiofiles + +import httpx + +import deprecation # type: ignore +from ..... import __version__ + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ....common import AbstractAsyncRestClient +from ....common import DeepgramError, DeepgramTypeError + +from .helpers import is_text_source +from .options import SpeakRESTOptions, FileSource +from .response import SpeakRESTResponse + + +class AsyncSpeakRESTClient(AbstractAsyncRestClient): + """ + A client class for doing Text-to-Speech. + Provides methods for speaking from text. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + super().__init__(config) + + # pylint: disable=too-many-positional-arguments + + async def stream_raw( + self, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> httpx.Response: + """ + Speak from a text source and store as a Iterator[byte]. + + Args: + source (TextSource): The text source to speak. + options (SpeakRESTOptions): Additional options for the ingest (default is None). + addons (Dict): Additional options for the request (default is None). + headers (Dict): Additional headers for the request (default is None). + timeout (httpx.Timeout): The timeout for the request (default is None). + endpoint (str): The endpoint to use for the request (default is "v1/speak"). + + Returns: + httpx.Response: The direct httpx.Response object from the speak request. + For more information, see https://www.python-httpx.org/api/#response + + IMPORTANT: The response object's `close()` method should be called when done + in order to prevent connection leaks. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AsyncSpeakClient.stream ENTER") + + url = f"{self._config.url}/{endpoint}" + if is_text_source(source): + body = source + else: + self._logger.error("Unknown speak source type") + self._logger.debug("AsyncSpeakClient.stream LEAVE") + raise DeepgramTypeError("Unknown speak source type") + + if isinstance(options, SpeakRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AsyncSpeakClient.stream LEAVE") + raise DeepgramError("Fatal speak options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, SpeakRESTOptions): + self._logger.info("SpeakRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + + result = await self.post_raw( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + + self._logger.info("result: %s", str(result)) + self._logger.notice("speak succeeded") + self._logger.debug("AsyncSpeakClient.stream LEAVE") + return result + + async def stream_memory( + self, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> SpeakRESTResponse: + """ + Speak from a text source and store in memory. + + Args: + source (TextSource): The text source to speak. + options (SpeakRESTOptions): Additional options for the ingest (default is None). + addons (Dict): Additional options for the request (default is None). + headers (Dict): Additional headers for the request (default is None). + timeout (httpx.Timeout): The timeout for the request (default is None). + endpoint (str): The endpoint to use for the request (default is "v1/speak"). + + Returns: + SpeakRESTResponse: The response from the speak request. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AsyncSpeakClient.stream ENTER") + + url = f"{self._config.url}/{endpoint}" + if is_text_source(source): + body = source + else: + self._logger.error("Unknown speak source type") + self._logger.debug("AsyncSpeakClient.stream LEAVE") + raise DeepgramTypeError("Unknown speak source type") + + if isinstance(options, SpeakRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AsyncSpeakClient.stream LEAVE") + raise DeepgramError("Fatal speak options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, SpeakRESTOptions): + self._logger.info("SpeakRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + + return_vals = [ + "content-type", + "request-id", + "model-uuid", + "model-name", + "char-count", + "transfer-encoding", + "date", + ] + result = await self.post_memory( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + file_result=return_vals, + **kwargs, + ) + self._logger.info("result: %s", result) + resp = SpeakRESTResponse( + content_type=str(result["content-type"]), + request_id=str(result["request-id"]), + model_uuid=str(result["model-uuid"]), + model_name=str(result["model-name"]), + characters=int(str(result["char-count"])), + transfer_encoding=str(result["transfer-encoding"]), + date=str(result["date"]), + stream=cast(io.BytesIO, result["stream"]), + stream_memory=cast(io.BytesIO, result["stream"]), + ) + self._logger.verbose("resp Object: %s", str(resp)) + self._logger.notice("speak succeeded") + self._logger.debug("AsyncSpeakClient.stream LEAVE") + return resp + + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="SpeakRESTClient.stream is deprecated. Use SpeakRESTClient.stream_memory instead.", + ) + async def stream( + self, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> SpeakRESTResponse: + """ + DEPRECATED: stream() is deprecated. Use stream_memory() instead. + """ + return await self.stream_memory( + source, + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + async def file( + self, + filename: str, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> SpeakRESTResponse: + """ + Speak from a text source and save to a file. + """ + return await self.save( + filename, + source, + options=options, + addons=addons, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + async def save( + self, + filename: str, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> SpeakRESTResponse: + """ + Speak from a text source and save to a file. + + Args: + source (TextSource): The text source to speak. + options (SpeakRESTOptions): Additional options for the ingest (default is None). + addons (Dict): Additional options for the request (default is None). + headers (Dict): Additional headers for the request (default is None). + timeout (httpx.Timeout): The timeout for the request (default is None). + endpoint (str): The endpoint to use for the request (default is "v1/speak"). + + Returns: + SpeakRESTResponse: The response from the speak request. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("AsyncSpeakClient.save ENTER") + + res = await self.stream_memory( + source, + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + if res.stream is None: + self._logger.error("stream is None") + self._logger.debug("AsyncSpeakClient.save LEAVE") + raise DeepgramError("BytesIO stream is None") + + # save to file + async with aiofiles.open(filename, "wb") as out: + await out.write(res.stream.getbuffer()) + await out.flush() + + # add filename to response + res.stream = None + res.filename = filename + + self._logger.debug("AsyncSpeakClient.save LEAVE") + return res + + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/speak/v1/rest/client.py b/deepgram/clients/speak/v1/rest/client.py new file mode 100644 index 0000000000000000000000000000000000000000..e17877e4a709f3cc5c10238ada7d2feeec4fc0dc --- /dev/null +++ b/deepgram/clients/speak/v1/rest/client.py @@ -0,0 +1,309 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from typing import Dict, Union, Optional, cast +import io + +import httpx + +import deprecation # type: ignore +from ..... import __version__ + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ....common import AbstractSyncRestClient +from ....common import DeepgramError, DeepgramTypeError +from .helpers import is_text_source + +from .options import SpeakRESTOptions, FileSource +from .response import SpeakRESTResponse + + +class SpeakRESTClient(AbstractSyncRestClient): + """ + A client class for doing Text-to-Speech. + Provides methods for speaking from text. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + super().__init__(config) + + # pylint: disable=too-many-positional-arguments + + def stream_raw( + self, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> httpx.Response: + """ + Speak from a text source and store as a Iterator[byte]. + + Args: + source (TextSource): The text source to speak. + options (SpeakRESTOptions): Additional options for the ingest (default is None). + addons (Dict): Additional options for the request (default is None). + headers (Dict): Additional headers for the request (default is None). + timeout (httpx.Timeout): The timeout for the request (default is None). + endpoint (str): The endpoint to use for the request (default is "v1/speak"). + + Returns: + httpx.Response: The direct httpx.Response object from the speak request. + For more information, see https://www.python-httpx.org/api/#response + + IMPORTANT: The response object's `close()` method should be called when done + in order to prevent connection leaks. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("SpeakClient.stream ENTER") + + url = f"{self._config.url}/{endpoint}" + if is_text_source(source): + body = source + else: + self._logger.error("Unknown speak source type") + self._logger.debug("SpeakClient.stream LEAVE") + raise DeepgramTypeError("Unknown speak source type") + + if isinstance(options, SpeakRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("SpeakClient.stream LEAVE") + raise DeepgramError("Fatal speak options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, SpeakRESTOptions): + self._logger.info("SpeakRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + + result = self.post_raw( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + **kwargs, + ) + + self._logger.info("result: %s", str(result)) + self._logger.notice("speak succeeded") + self._logger.debug("SpeakClient.stream LEAVE") + return result + + def stream_memory( + self, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> SpeakRESTResponse: + """ + Speak from a text source and store in memory. + + Args: + source (TextSource): The text source to speak. + options (SpeakRESTOptions): Additional options for the ingest (default is None). + addons (Dict): Additional options for the request (default is None). + headers (Dict): Additional headers for the request (default is None). + timeout (httpx.Timeout): The timeout for the request (default is None). + endpoint (str): The endpoint to use for the request (default is "v1/speak"). + + Returns: + SpeakRESTResponse: The response from the speak request. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("SpeakClient.stream ENTER") + + url = f"{self._config.url}/{endpoint}" + if is_text_source(source): + body = source + else: + self._logger.error("Unknown speak source type") + self._logger.debug("SpeakClient.stream LEAVE") + raise DeepgramTypeError("Unknown speak source type") + + if isinstance(options, SpeakRESTOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("SpeakClient.stream LEAVE") + raise DeepgramError("Fatal speak options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, SpeakRESTOptions): + self._logger.info("SpeakRESTOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + + return_vals = [ + "content-type", + "request-id", + "model-uuid", + "model-name", + "char-count", + "transfer-encoding", + "date", + ] + result = self.post_memory( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + file_result=return_vals, + **kwargs, + ) + + self._logger.info("result: %s", result) + resp = SpeakRESTResponse( + content_type=str(result["content-type"]), + request_id=str(result["request-id"]), + model_uuid=str(result["model-uuid"]), + model_name=str(result["model-name"]), + characters=int(str(result["char-count"])), + transfer_encoding=str(result["transfer-encoding"]), + date=str(result["date"]), + stream=cast(io.BytesIO, result["stream"]), + stream_memory=cast(io.BytesIO, result["stream"]), + ) + self._logger.verbose("resp Object: %s", resp) + self._logger.notice("speak succeeded") + self._logger.debug("SpeakClient.stream LEAVE") + return resp + + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="SpeakRESTClient.stream is deprecated. Use SpeakRESTClient.stream_memory instead.", + ) + def stream( + self, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> SpeakRESTResponse: + """ + DEPRECATED: stream() is deprecated. Use stream_memory() instead. + """ + return self.stream_memory( + source, + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + async def file( + self, + filename: str, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> SpeakRESTResponse: + """ + Speak from a text source and save to a file. + """ + return self.save( + filename, + source, + options=options, + addons=addons, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + def save( + self, + filename: str, + source: FileSource, + options: Optional[Union[Dict, SpeakRESTOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/speak", + **kwargs, + ) -> SpeakRESTResponse: + """ + Speak from a text source and save to a file. + + Args: + source (TextSource): The text source to speak. + options (SpeakRESTOptions): Additional options for the ingest (default is None). + addons (Dict): Additional options for the request (default is None). + headers (Dict): Additional headers for the request (default is None). + timeout (httpx.Timeout): The timeout for the request (default is None). + endpoint (str): The endpoint to use for the request (default is "v1/speak"). + + Returns: + SpeakRESTResponse: The response from the speak request. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("SpeakClient.save ENTER") + + res = self.stream_memory( + source, + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + **kwargs, + ) + + if res.stream is None: + self._logger.error("stream is None") + self._logger.debug("SpeakClient.save LEAVE") + raise DeepgramError("BytesIO stream is None") + + # save to file + with open(filename, "wb+") as file: + file.write(res.stream.getbuffer()) + file.flush() + + # add filename to response + res.stream = None + res.filename = filename + + self._logger.debug("SpeakClient.save LEAVE") + return res + + # pylint: enable=too-many-positional-arguments diff --git a/deepgram/clients/speak/v1/rest/helpers.py b/deepgram/clients/speak/v1/rest/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..3232c1e7884d26920fa5d0d3bc4f3d085f14433b --- /dev/null +++ b/deepgram/clients/speak/v1/rest/helpers.py @@ -0,0 +1,19 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .options import SpeakSource + + +def is_text_source(provided_source: SpeakSource) -> bool: + """ + Check if the provided source is a text source. + """ + return "text" in provided_source + + +def is_readstream_source(provided_source: SpeakSource) -> bool: + """ + Check if the provided source is a readstream source. + """ + return "stream" in provided_source diff --git a/deepgram/clients/speak/v1/rest/options.py b/deepgram/clients/speak/v1/rest/options.py new file mode 100644 index 0000000000000000000000000000000000000000..504c3ae11c2b977bbe4f692dd36f4ffa8d79bcc8 --- /dev/null +++ b/deepgram/clients/speak/v1/rest/options.py @@ -0,0 +1,64 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from io import BufferedReader +from typing import Union, Optional +import logging + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from .....utils import verboselogs +from ....common import TextSource, BufferSource, StreamSource, FileSource, BaseResponse + + +@dataclass +class SpeakRESTOptions(BaseResponse): + """ + Contains all the options for the SpeakOptions. + + Reference: + https://developers.deepgram.com/reference/text-to-speech-api + """ + + model: Optional[str] = field( + default="aura-asteria-en", + metadata=dataclass_config(exclude=lambda f: f is None), + ) + encoding: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + container: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + sample_rate: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + bit_rate: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def check(self): + """ + Check the SpeakOptions for any missing or invalid values. + """ + logger = verboselogs.VerboseLogger(__name__) + logger.addHandler(logging.StreamHandler()) + prev = logger.level + logger.setLevel(verboselogs.ERROR) + + # no op at the moment + + logger.setLevel(prev) + + return True + + +SpeakOptions = SpeakRESTOptions + + +# unqiue +SpeakSource = Union[FileSource, BufferedReader] +SpeakRestSource = SpeakSource +SpeakRESTSource = SpeakSource # pylint: disable=invalid-name diff --git a/deepgram/clients/speak/v1/rest/response.py b/deepgram/clients/speak/v1/rest/response.py new file mode 100644 index 0000000000000000000000000000000000000000..30801e3060d0e86eb7bcb9c6a637c0f131fe9422 --- /dev/null +++ b/deepgram/clients/speak/v1/rest/response.py @@ -0,0 +1,45 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from typing import Optional, Dict, Any +import io + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from ....common import ( + BaseResponse, +) + + +# Speak Response Types: + + +@dataclass +class SpeakRESTResponse(BaseResponse): # pylint: disable=too-many-instance-attributes + """ + A class for representing a response from the speak endpoint. + """ + + content_type: str = "" + request_id: str = "" + model_uuid: str = "" + model_name: str = "" + characters: int = 0 + transfer_encoding: str = "" + date: str = "" + filename: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + # pylint: disable=W0511 + # TODO: stream will be deprecated in a future release. Please use stream_memory instead. + stream: Optional[io.BytesIO] = field( + default=None, + metadata=dataclass_config(exclude=lambda f: True), + ) + # pylint: enable=W0511 + stream_memory: Optional[io.BytesIO] = field( + default=None, + metadata=dataclass_config(exclude=lambda f: True), + ) diff --git a/deepgram/clients/speak/v1/websocket/__init__.py b/deepgram/clients/speak/v1/websocket/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5245b93485fa78dd6b9ff2a607ed68249eda345f --- /dev/null +++ b/deepgram/clients/speak/v1/websocket/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from .client import SpeakWebSocketClient, SpeakWSClient +from .async_client import AsyncSpeakWebSocketClient, AsyncSpeakWSClient +from .response import ( + #### top level + MetadataResponse, + FlushedResponse, + ClearedResponse, + WarningResponse, + #### shared + OpenResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) +from .options import SpeakWSOptions diff --git a/deepgram/clients/speak/v1/websocket/__pycache__/__init__.cpython-313.pyc b/deepgram/clients/speak/v1/websocket/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e281b4b701065a8fdbad88d87ab6701e42653120 Binary files /dev/null and b/deepgram/clients/speak/v1/websocket/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/websocket/__pycache__/async_client.cpython-313.pyc b/deepgram/clients/speak/v1/websocket/__pycache__/async_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..654ece4d4dcd06df8ab721a74a93a41f23a2658d Binary files /dev/null and b/deepgram/clients/speak/v1/websocket/__pycache__/async_client.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/websocket/__pycache__/client.cpython-313.pyc b/deepgram/clients/speak/v1/websocket/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebac06eb9cc535f3afbdbdf2620f1c9dce50db3a Binary files /dev/null and b/deepgram/clients/speak/v1/websocket/__pycache__/client.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/websocket/__pycache__/options.cpython-313.pyc b/deepgram/clients/speak/v1/websocket/__pycache__/options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09765de219eee127489acbb38286525d8c6926ec Binary files /dev/null and b/deepgram/clients/speak/v1/websocket/__pycache__/options.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/websocket/__pycache__/response.cpython-313.pyc b/deepgram/clients/speak/v1/websocket/__pycache__/response.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..539bd6864d9fa0d29077d436603df602fde1cedc Binary files /dev/null and b/deepgram/clients/speak/v1/websocket/__pycache__/response.cpython-313.pyc differ diff --git a/deepgram/clients/speak/v1/websocket/async_client.py b/deepgram/clients/speak/v1/websocket/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..c69debcf410b559782501d082d2deb786a1ee774 --- /dev/null +++ b/deepgram/clients/speak/v1/websocket/async_client.py @@ -0,0 +1,706 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import asyncio +import json +import logging +from typing import Dict, Union, Optional, cast, Any, Callable +from datetime import datetime +import threading + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import SpeakWebSocketEvents, SpeakWebSocketMessage +from ....common import AbstractAsyncWebSocketClient +from ....common import DeepgramError + +from .response import ( + OpenResponse, + MetadataResponse, + FlushedResponse, + ClearedResponse, + CloseResponse, + WarningResponse, + ErrorResponse, + UnhandledResponse, +) +from .options import SpeakWSOptions + +from .....audio.microphone import Microphone +from .....audio.speaker import Speaker, RATE, CHANNELS, PLAYBACK_DELTA + +ONE_SECOND = 1 +HALF_SECOND = 0.5 +DEEPGRAM_INTERVAL = 5 +PING_INTERVAL = 20 + + +class AsyncSpeakWSClient( + AbstractAsyncWebSocketClient +): # pylint: disable=too-many-instance-attributes + """ + Client for interacting with Deepgram's text-to-speech services over WebSockets. + + This class provides methods to establish a WebSocket connection for TTS synthesis and handle real-time TTS synthesis events. + + Args: + config (DeepgramClientOptions): all the options for the client. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + _event_handlers: Dict[SpeakWebSocketEvents, list] + + _flush_thread: Union[asyncio.Task, None] + _last_datagram: Optional[datetime] = None + _flush_count: int + + _kwargs: Optional[Dict] = None + _addons: Optional[Dict] = None + _options: Optional[Dict] = None + _headers: Optional[Dict] = None + + _speaker_created: bool = False + _speaker: Optional[Speaker] = None + _microphone: Optional[Microphone] = None + + def __init__( + self, config: DeepgramClientOptions, microphone: Optional[Microphone] = None + ): + if config is None: + raise DeepgramError("Config is required") + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + self._endpoint = "v1/speak" + + self._flush_thread = None + + # auto flush + self._last_datagram = None + self._flush_count = 0 + + # microphone + self._microphone = microphone + + # init handlers + self._event_handlers = { + event: [] for event in SpeakWebSocketEvents.__members__.values() + } + + if self._config.options.get("speaker_playback") == "true": + self._logger.info("speaker_playback is enabled") + rate = self._config.options.get("speaker_playback_rate") + if rate is None: + rate = RATE + channels = self._config.options.get("speaker_playback_channels") + if channels is None: + channels = CHANNELS + playback_delta_in_ms = self._config.options.get( + "speaker_playback_delta_in_ms" + ) + if playback_delta_in_ms is None: + playback_delta_in_ms = PLAYBACK_DELTA + device_index = self._config.options.get("speaker_playback_device_index") + + self._logger.debug("rate: %s", rate) + self._logger.debug("channels: %s", channels) + self._logger.debug("device_index: %s", device_index) + + self._speaker_created = True + + if device_index is not None: + self._speaker = Speaker( + rate=rate, + channels=channels, + last_play_delta_in_ms=playback_delta_in_ms, + verbose=self._config.verbose, + output_device_index=device_index, + microphone=self._microphone, + ) + else: + self._speaker = Speaker( + rate=rate, + channels=channels, + last_play_delta_in_ms=playback_delta_in_ms, + verbose=self._config.verbose, + microphone=self._microphone, + ) + + # call the parent constructor + super().__init__(self._config, self._endpoint) + + # pylint: disable=too-many-branches,too-many-statements + async def start( + self, + options: Optional[Union[SpeakWSOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + members: Optional[Dict] = None, + **kwargs, + ) -> bool: + """ + Starts the WebSocket connection for text-to-speech synthesis. + """ + self._logger.debug("AsyncSpeakWebSocketClient.start ENTER") + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + self._logger.info("members: %s", members) + self._logger.info("kwargs: %s", kwargs) + + if isinstance(options, SpeakWSOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("AsyncSpeakWebSocketClient.start LEAVE") + raise DeepgramError("Fatal text-to-speech options error") + + self._addons = addons + self._headers = headers + + # add "members" as members of the class + if members is not None: + self.__dict__.update(members) + + # set kwargs as members of the class + if kwargs is not None: + self._kwargs = kwargs + else: + self._kwargs = {} + + if isinstance(options, SpeakWSOptions): + self._logger.info("SpeakWSOptions switching class -> dict") + self._options = options.to_dict() + elif options is not None: + self._options = options + else: + self._options = {} + + try: + # speaker substitutes the listening thread + if self._speaker is not None: + self._logger.notice("passing speaker to delegate_listening") + super().delegate_listening(self._speaker) + + # call parent start + if ( + await super().start( + self._options, + self._addons, + self._headers, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + is False + ): + self._logger.error("AsyncSpeakWebSocketClient.start failed") + self._logger.debug("AsyncSpeakWebSocketClient.start LEAVE") + return False + + if self._speaker is not None: + self._logger.notice("start delegate_listening thread") + self._speaker.start() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # flush thread + if self._config.is_auto_flush_speak_enabled(): + self._logger.notice("autoflush is enabled") + self._flush_thread = asyncio.create_task(self._flush()) + else: + self._logger.notice("autoflush is disabled") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("start succeeded") + self._logger.debug("AsyncSpeakWebSocketClient.start LEAVE") + return True + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "WebSocketException in AsyncSpeakWebSocketClient.start: %s", e + ) + self._logger.debug("AsyncSpeakWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect") is True: + raise + return False + + # pylint: enable=too-many-branches,too-many-statements + + def on(self, event: SpeakWebSocketEvents, handler: Callable) -> None: + """ + Registers event handlers for specific events. + """ + self._logger.info("event subscribed: %s", event) + if event in SpeakWebSocketEvents.__members__.values() and callable(handler): + self._event_handlers[event].append(handler) + + # triggers the registered event handlers for a specific event + async def _emit(self, event: SpeakWebSocketEvents, *args, **kwargs) -> None: + """ + Emits events to the registered event handlers. + """ + self._logger.debug("AsyncSpeakWebSocketClient._emit ENTER") + self._logger.debug("callback handlers for: %s", event) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + tasks = [] + for handler in self._event_handlers[event]: + task = asyncio.create_task(handler(self, *args, **kwargs)) + tasks.append(task) + + if tasks: + self._logger.debug("waiting for tasks to finish...") + await asyncio.gather(*filter(None, tasks), return_exceptions=True) + tasks.clear() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("AsyncSpeakWebSocketClient._emit LEAVE") + + async def _process_text(self, message: Union[str, bytes]) -> None: + """ + Processes messages received over the WebSocket connection. + """ + self._logger.debug("AsyncSpeakWebSocketClient._process_text ENTER") + + try: + self._logger.debug("Text data received") + + if len(message) == 0: + self._logger.debug("message is empty") + self._logger.debug("AsyncSpeakWebSocketClient._process_text LEAVE") + return + + data = json.loads(message) + response_type = data.get("type") + self._logger.debug("response_type: %s, data: %s", response_type, data) + + match response_type: + case SpeakWebSocketEvents.Open: + open_result: OpenResponse = OpenResponse.from_json(message) + self._logger.verbose("OpenResponse: %s", open_result) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Open), + open=open_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Metadata: + meta_result: MetadataResponse = MetadataResponse.from_json(message) + self._logger.verbose("MetadataResponse: %s", meta_result) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Metadata), + metadata=meta_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Flushed: + fl_result: FlushedResponse = FlushedResponse.from_json(message) + self._logger.verbose("FlushedResponse: %s", fl_result) + + # auto flush + if self._config.is_inspecting_speak(): + self._flush_count -= 1 + self._logger.debug( + "Decrement AutoFlush count: %d", + self._flush_count, + ) + + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Flushed), + flushed=fl_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Cleared: + clear_result: ClearedResponse = ClearedResponse.from_json(message) + self._logger.verbose("ClearedResponse: %s", clear_result) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Cleared), + cleared=clear_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Close: + close_result: CloseResponse = CloseResponse.from_json(message) + self._logger.verbose("CloseResponse: %s", close_result) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Close), + close=close_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Warning: + war_warning: WarningResponse = WarningResponse.from_json(message) + self._logger.verbose("WarningResponse: %s", war_warning) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Warning), + warning=war_warning, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Error: + err_error: ErrorResponse = ErrorResponse.from_json(message) + self._logger.verbose("ErrorResponse: %s", err_error) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case _: + self._logger.warning( + "Unknown Message: response_type: %s, data: %s", + response_type, + data, + ) + unhandled_error: UnhandledResponse = UnhandledResponse( + type=SpeakWebSocketEvents(SpeakWebSocketEvents.Unhandled), + raw=str(message), + ) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Unhandled), + unhandled=unhandled_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_text Succeeded") + self._logger.debug("AsyncSpeakWebSocketClient._process_text LEAVE") + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AsyncSpeakWebSocketClient._process_text: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AsyncSpeakWebSocketClient._process_text", + f"{e}", + "Exception", + ) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await super()._signal_exit() + + self._logger.debug("AsyncSpeakWebSocketClient._process_text LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-return-statements,too-many-statements + + async def _process_binary(self, message: bytes) -> None: + self._logger.debug("SpeakWebSocketClient._process_binary ENTER") + self._logger.debug("Binary data received") + + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.AudioData), + data=message, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_binary Succeeded") + self._logger.debug("SpeakWebSocketClient._process_binary LEAVE") + + ## pylint: disable=too-many-return-statements + async def _flush(self) -> None: + self._logger.debug("AsyncSpeakWebSocketClient._flush ENTER") + + delta_in_ms_str = self._config.options.get("auto_flush_speak_delta") + if delta_in_ms_str is None: + self._logger.error("auto_flush_speak_delta is None") + self._logger.debug("AsyncSpeakWebSocketClient._flush LEAVE") + return + delta_in_ms = float(delta_in_ms_str) + + while True: + try: + await asyncio.sleep(HALF_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_flush exiting gracefully") + self._logger.debug("AsyncSpeakWebSocketClient._flush LEAVE") + return + + if self._last_datagram is None: + self._logger.debug("AutoFlush last_datagram is None") + continue + + delta = datetime.now() - self._last_datagram + diff_in_ms = delta.total_seconds() * 1000 + self._logger.debug("AutoFlush delta: %f", diff_in_ms) + if diff_in_ms < delta_in_ms: + self._logger.debug("AutoFlush delta is less than threshold") + continue + + await self.flush() + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in AsyncSpeakWebSocketClient._flush: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in AsyncSpeakWebSocketClient._flush", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in AsyncSpeakWebSocketClient._flush: %s", str(e) + ) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + await super()._signal_exit() + + self._logger.debug("AsyncSpeakWebSocketClient._flush LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-return-statements + + async def send_text(self, text_input: str) -> bool: + """ + Sends text to the WebSocket connection to generate audio. + + Args: + text_input (str): The raw text to be synthesized. This function will automatically wrap + the text in a JSON object of type "Speak" with the key "text". + + Returns: + bool: True if the text was successfully sent, False otherwise. + """ + return await self.send_raw(json.dumps({"type": "Speak", "text": text_input})) + + async def send(self, data: Union[bytes, str]) -> bool: + """ + Alias for send_text. Please see send_text for more information. + """ + if isinstance(data, bytes): + self._logger.error("send() failed - data is bytes") + return False + + return await self.send_text(data) + + # pylint: disable=unused-argument + async def send_control( + self, msg_type: Union[SpeakWebSocketMessage, str], data: Optional[str] = "" + ) -> bool: + """ + Sends a control message consisting of type SpeakWebSocketEvents over the WebSocket connection. + + Args: + msg_type (SpeakWebSocketEvents): The type of control message to send. + (Optional) data (str): The data to send with the control message. + + Returns: + bool: True if the control message was successfully sent, False otherwise. + """ + control_msg = json.dumps({"type": msg_type}) + return await self.send_raw(control_msg) + + # pylint: enable=unused-argument + + # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements + async def send_raw(self, msg: str) -> bool: + """ + Sends a raw/control message over the WebSocket connection. This message must contain a valid JSON object. + + Args: + msg (str): The raw message to send over the WebSocket connection. + + Returns: + bool: True if the message was successfully sent, False otherwise. + """ + self._logger.spam("AsyncSpeakWebSocketClient.send_raw ENTER") + + if self._config.is_inspecting_speak(): + try: + _tmp_json = json.loads(msg) + if "type" in _tmp_json: + self._logger.debug( + "Inspecting Message: Sending %s", _tmp_json["type"] + ) + match _tmp_json["type"]: + case SpeakWebSocketMessage.Speak: + inspect_res = await self._inspect() + if not inspect_res: + self._logger.error("inspect_res failed") + case SpeakWebSocketMessage.Flush: + self._last_datagram = None + self._flush_count += 1 + self._logger.debug( + "Increment Flush count: %d", self._flush_count + ) + except Exception as e: # pylint: disable=broad-except + self._logger.error("send_raw() failed - Exception: %s", str(e)) + + try: + if await super().send(msg) is False: + self._logger.error("send_raw() failed") + self._logger.spam("AsyncSpeakWebSocketClient.send_raw LEAVE") + return False + self._logger.spam("send_raw() succeeded") + self._logger.spam("AsyncSpeakWebSocketClient.send_raw LEAVE") + return True + except Exception as e: # pylint: disable=broad-except + self._logger.error("send_raw() failed - Exception: %s", str(e)) + self._logger.spam("AsyncSpeakWebSocketClient.send_raw LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return False + + # pylint: enable=too-many-return-statements,too-many-branches + + async def flush(self) -> bool: + """ + Flushes the current buffer and returns generated audio + """ + self._logger.spam("AsyncSpeakWebSocketClient.flush ENTER") + + self._logger.notice("Sending Flush...") + ret = await self.send_control(SpeakWebSocketMessage.Flush) + + if not ret: + self._logger.error("flush failed") + self._logger.spam("AsyncSpeakWebSocketClient.flush LEAVE") + return False + + self._logger.notice("flush succeeded") + self._logger.spam("AsyncSpeakWebSocketClient.flush LEAVE") + + return True + + async def clear(self) -> bool: + """ + Clears the current buffer on the server + """ + self._logger.spam("AsyncSpeakWebSocketClient.clear ENTER") + + self._logger.notice("Sending Clear...") + ret = await self.send_control(SpeakWebSocketMessage.Clear) + + if not ret: + self._logger.error("clear failed") + self._logger.spam("AsyncSpeakWebSocketClient.clear LEAVE") + return False + + self._logger.notice("clear succeeded") + self._logger.spam("AsyncSpeakWebSocketClient.clear LEAVE") + + return True + + async def wait_for_complete(self): + """ + This method will block until the speak is done playing sound. + """ + self._logger.spam("AsyncSpeakWebSocketClient.wait_for_complete ENTER") + + if self._speaker is None: + self._logger.error("speaker is None. Return immediately") + return + + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self._speaker.wait_for_complete) + self._logger.notice("wait_for_complete succeeded") + self._logger.spam("AsyncSpeakWebSocketClient.wait_for_complete LEAVE") + + async def _close_message(self) -> bool: + return await self.send_control(SpeakWebSocketMessage.Close) + + async def finish(self) -> bool: + """ + Closes the WebSocket connection gracefully. + """ + self._logger.debug("AsyncSpeakWebSocketClient.finish ENTER") + + # stop the threads + self._logger.verbose("cancelling tasks...") + try: + # call parent finish + if await super().finish() is False: + self._logger.error("AsyncListenWebSocketClient.finish failed") + + if self._speaker is not None and self._speaker_created: + self._speaker.finish() + self._speaker_created = False + + # Before cancelling, check if the tasks were created + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + tasks = [] + + if self._speaker is not None: + self._logger.notice("stopping speaker...") + self._speaker.finish() + self._speaker = None + self._logger.notice("speaker stopped") + + if self._flush_thread is not None: + self._logger.notice("stopping _flush_thread...") + self._flush_thread.cancel() + tasks.append(self._flush_thread) + self._logger.notice("_flush_thread cancelled") + + # Use asyncio.gather to wait for tasks to be cancelled + # Prevent indefinite waiting by setting a timeout + await asyncio.wait_for(asyncio.gather(*tasks), timeout=10) + self._logger.notice("threads joined") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("finish succeeded") + self._logger.spam("AsyncSpeakWebSocketClient.finish LEAVE") + return True + + except asyncio.CancelledError: + self._logger.debug("tasks cancelled") + self._logger.debug("AsyncSpeakWebSocketClient.finish LEAVE") + return False + + except asyncio.TimeoutError as e: + self._logger.error("tasks cancellation timed out: %s", e) + self._logger.debug("AsyncSpeakWebSocketClient.finish LEAVE") + return False + + async def _inspect(self) -> bool: + # auto flush_inspect is generically used to track any messages you might want to snoop on + # place additional logic here to inspect messages of interest + + # for auto flush functionality + # set the last datagram + self._last_datagram = datetime.now() + self._logger.debug( + "AutoFlush last received: %s", + str(self._last_datagram), + ) + + return True + + +AsyncSpeakWebSocketClient = AsyncSpeakWSClient diff --git a/deepgram/clients/speak/v1/websocket/client.py b/deepgram/clients/speak/v1/websocket/client.py new file mode 100644 index 0000000000000000000000000000000000000000..d14c3603c405bfff6dd269b24dddbcb386366df2 --- /dev/null +++ b/deepgram/clients/speak/v1/websocket/client.py @@ -0,0 +1,686 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import json +import time +import logging +from typing import Dict, Union, Optional, cast, Any, Callable +from datetime import datetime +import threading + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import SpeakWebSocketEvents, SpeakWebSocketMessage +from ....common import AbstractSyncWebSocketClient +from ....common import DeepgramError + +from .response import ( + OpenResponse, + MetadataResponse, + FlushedResponse, + ClearedResponse, + CloseResponse, + WarningResponse, + ErrorResponse, + UnhandledResponse, +) +from .options import SpeakWSOptions + +from .....audio.microphone import Microphone +from .....audio.speaker import Speaker, RATE, CHANNELS, PLAYBACK_DELTA + +ONE_SECOND = 1 +HALF_SECOND = 0.5 +DEEPGRAM_INTERVAL = 5 +PING_INTERVAL = 20 + + +class SpeakWSClient( + AbstractSyncWebSocketClient +): # pylint: disable=too-many-instance-attributes + """ + Client for interacting with Deepgram's text-to-speech services over WebSockets. + + This class provides methods to establish a WebSocket connection for TTS synthesis and handle real-time TTS synthesis events. + + Args: + config (DeepgramClientOptions): all the options for the client. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + + _event_handlers: Dict[SpeakWebSocketEvents, list] + + _flush_thread: Union[threading.Thread, None] + _lock_flush: threading.Lock + _last_datagram: Optional[datetime] = None + _flush_count: int + + _kwargs: Optional[Dict] = None + _addons: Optional[Dict] = None + _options: Optional[Dict] = None + _headers: Optional[Dict] = None + + _speaker_created: bool = False + _speaker: Optional[Speaker] = None + _microphone: Optional[Microphone] = None + + def __init__( + self, config: DeepgramClientOptions, microphone: Optional[Microphone] = None + ): + if config is None: + raise DeepgramError("Config is required") + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + self._endpoint = "v1/speak" + self._lock_flush = threading.Lock() + + self._flush_thread = None + + # auto flush + self._last_datagram = None + self._flush_count = 0 + + # microphone + self._microphone = microphone + + # init handlers + self._event_handlers = { + event: [] for event in SpeakWebSocketEvents.__members__.values() + } + + if self._config.options.get("speaker_playback") == "true": + self._logger.info("speaker_playback is enabled") + rate = self._config.options.get("speaker_playback_rate") + if rate is None: + rate = RATE + channels = self._config.options.get("speaker_playback_channels") + if channels is None: + channels = CHANNELS + playback_delta_in_ms = self._config.options.get( + "speaker_playback_delta_in_ms" + ) + if playback_delta_in_ms is None: + playback_delta_in_ms = PLAYBACK_DELTA + device_index = self._config.options.get("speaker_playback_device_index") + + self._logger.debug("rate: %s", rate) + self._logger.debug("channels: %s", channels) + self._logger.debug("device_index: %s", device_index) + + self._speaker_created = True + + if device_index is not None: + self._speaker = Speaker( + rate=rate, + channels=channels, + last_play_delta_in_ms=playback_delta_in_ms, + verbose=self._config.verbose, + output_device_index=device_index, + microphone=self._microphone, + ) + else: + self._speaker = Speaker( + rate=rate, + channels=channels, + last_play_delta_in_ms=playback_delta_in_ms, + verbose=self._config.verbose, + microphone=self._microphone, + ) + + # call the parent constructor + super().__init__(self._config, self._endpoint) + + # pylint: disable=too-many-statements,too-many-branches + def start( + self, + options: Optional[Union[SpeakWSOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + members: Optional[Dict] = None, + **kwargs, + ) -> bool: + """ + Starts the WebSocket connection for text-to-speech synthesis. + """ + self._logger.debug("SpeakWebSocketClient.start ENTER") + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + self._logger.info("members: %s", members) + self._logger.info("kwargs: %s", kwargs) + + if isinstance(options, SpeakWSOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("SpeakWebSocketClient.start LEAVE") + raise DeepgramError("Fatal text-to-speech options error") + + self._addons = addons + self._headers = headers + + # add "members" as members of the class + if members is not None: + self.__dict__.update(members) + + # set kwargs as members of the class + if kwargs is not None: + self._kwargs = kwargs + else: + self._kwargs = {} + + if isinstance(options, SpeakWSOptions): + self._logger.info("SpeakWSOptions switching class -> dict") + self._options = options.to_dict() + elif options is not None: + self._options = options + else: + self._options = {} + + try: + # speaker substitutes the listening thread + if self._speaker is not None: + self._logger.notice("passing speaker to delegate_listening") + super().delegate_listening(self._speaker) + + # call parent start + if ( + super().start( + self._options, + self._addons, + self._headers, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + is False + ): + self._logger.error("SpeakWebSocketClient.start failed") + self._logger.debug("SpeakWebSocketClient.start LEAVE") + return False + + if self._speaker is not None: + self._logger.notice("start delegate_listening thread") + self._speaker.start() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # flush thread + if self._config.is_auto_flush_speak_enabled(): + self._logger.notice("autoflush is enabled") + self._flush_thread = threading.Thread(target=self._flush) + self._flush_thread.start() + else: + self._logger.notice("autoflush is disabled") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("start succeeded") + self._logger.debug("SpeakWebSocketClient.start LEAVE") + return True + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "WebSocketException in SpeakWebSocketClient.start: %s", e + ) + self._logger.debug("SpeakWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect") is True: + raise + return False + + # pylint: enable=too-many-statements,too-many-branches + + def on(self, event: SpeakWebSocketEvents, handler: Callable) -> None: + """ + Registers event handlers for specific events. + """ + self._logger.info("event subscribed: %s", event) + if event in SpeakWebSocketEvents.__members__.values() and callable(handler): + self._event_handlers[event].append(handler) + + def _emit(self, event: SpeakWebSocketEvents, *args, **kwargs) -> None: + """ + Emits events to the registered event handlers. + """ + self._logger.debug("SpeakWebSocketClient._emit ENTER") + self._logger.debug("callback handlers for: %s", event) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("callback handlers for: %s", event) + for handler in self._event_handlers[event]: + handler(self, *args, **kwargs) + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.debug("ListenWebSocketClient._emit LEAVE") + + def _process_text(self, message: str) -> None: + """ + Processes messages received over the WebSocket connection. + """ + self._logger.debug("SpeakWebSocketClient._process_text ENTER") + + try: + self._logger.debug("Text data received") + + if len(message) == 0: + self._logger.debug("message is empty") + self._logger.debug("SpeakWebSocketClient._process_text LEAVE") + return + + data = json.loads(message) + response_type = data.get("type") + self._logger.debug("response_type: %s, data: %s", response_type, data) + + match response_type: + case SpeakWebSocketEvents.Open: + open_result: OpenResponse = OpenResponse.from_json(message) + self._logger.verbose("OpenResponse: %s", open_result) + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Open), + open=open_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Metadata: + meta_result: MetadataResponse = MetadataResponse.from_json(message) + self._logger.verbose("MetadataResponse: %s", meta_result) + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Metadata), + metadata=meta_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Flushed: + fl_result: FlushedResponse = FlushedResponse.from_json(message) + self._logger.verbose("FlushedResponse: %s", fl_result) + + # auto flush + if self._config.is_inspecting_speak(): + with self._lock_flush: + self._flush_count -= 1 + self._logger.debug( + "Decrement Flush count: %d", + self._flush_count, + ) + + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Flushed), + flushed=fl_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Cleared: + clear_result: ClearedResponse = ClearedResponse.from_json(message) + self._logger.verbose("ClearedResponse: %s", clear_result) + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Cleared), + cleared=clear_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Close: + close_result: CloseResponse = CloseResponse.from_json(message) + self._logger.verbose("CloseResponse: %s", close_result) + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Close), + close=close_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Warning: + war_warning: WarningResponse = WarningResponse.from_json(message) + self._logger.verbose("WarningResponse: %s", war_warning) + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Warning), + warning=war_warning, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Error: + err_error: ErrorResponse = ErrorResponse.from_json(message) + self._logger.verbose("ErrorResponse: %s", err_error) + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case _: + self._logger.warning( + "Unknown Message: response_type: %s, data: %s", + response_type, + data, + ) + unhandled_error: UnhandledResponse = UnhandledResponse( + type=SpeakWebSocketEvents(SpeakWebSocketEvents.Unhandled), + raw=message, + ) + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Unhandled), + unhandled=unhandled_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_text Succeeded") + self._logger.debug("SpeakWebSocketClient._process_text LEAVE") + + except Exception as e: # pylint: disable=broad-except + self._logger.error("Exception in SpeakWebSocketClient._process_text: %s", e) + e_error: ErrorResponse = ErrorResponse( + "Exception in SpeakWebSocketClient._process_text", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in SpeakWebSocketClient._process_text: %s", str(e) + ) + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), + e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + super()._signal_exit() + + self._logger.debug("SpeakWebSocketClient._process_text LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-return-statements,too-many-statements + + def _process_binary(self, message: bytes) -> None: + self._logger.debug("SpeakWebSocketClient._process_binary ENTER") + self._logger.debug("Binary data received") + + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.AudioData), + data=message, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + self._logger.notice("_process_binary Succeeded") + self._logger.debug("SpeakWebSocketClient._process_binary LEAVE") + + # pylint: disable=too-many-return-statements + def _flush(self) -> None: + self._logger.debug("SpeakWebSocketClient._flush ENTER") + + delta_in_ms_str = self._config.options.get("auto_flush_speak_delta") + if delta_in_ms_str is None: + self._logger.error("auto_flush_speak_delta is None") + self._logger.debug("SpeakWebSocketClient._flush LEAVE") + return + delta_in_ms = float(delta_in_ms_str) + + while True: + try: + self._exit_event.wait(timeout=HALF_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_flush exiting gracefully") + self._logger.debug("ListenWebSocketClient._flush LEAVE") + return + + if self._last_datagram is None: + self._logger.debug("AutoFlush last_datagram is None") + continue + + with self._lock_flush: + delta = datetime.now() - self._last_datagram + diff_in_ms = delta.total_seconds() * 1000 + self._logger.debug("AutoFlush delta: %f", diff_in_ms) + if diff_in_ms < delta_in_ms: + self._logger.debug("AutoFlush delta is less than threshold") + continue + + self.flush() + + except Exception as e: # pylint: disable=broad-except + self._logger.error("Exception in SpeakWebSocketClient._flush: %s", e) + e_error: ErrorResponse = ErrorResponse( + "Exception in SpeakWebSocketClient._flush", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in SpeakWebSocketClient._flush: %s", str(e) + ) + self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), + error=e_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + # signal exit and close + super()._signal_exit() + + self._logger.debug("SpeakWebSocketClient._flush LEAVE") + + if self._config.options.get("termination_exception") is True: + raise + return + + # pylint: enable=too-many-return-statements + + def send_text(self, text_input: str) -> bool: + """ + Sends text to the WebSocket connection to generate audio. + + Args: + text_input (str): The raw text to be synthesized. This function will automatically wrap + the text in a JSON object of type "Speak" with the key "text". + + Returns: + bool: True if the text was successfully sent, False otherwise. + """ + return self.send_raw(json.dumps({"type": "Speak", "text": text_input})) + + def send(self, data: Union[str, bytes]) -> bool: + """ + Alias for send_text. Please see send_text for more information. + """ + if isinstance(data, bytes): + self._logger.error("send() failed - data is bytes") + return False + + return self.send_text(data) + + # pylint: disable=unused-argument + def send_control( + self, msg_type: Union[SpeakWebSocketMessage, str], data: Optional[str] = "" + ) -> bool: + """ + Sends a control message consisting of type SpeakWebSocketEvents over the WebSocket connection. + + Args: + msg_type (SpeakWebSocketEvents): The type of control message to send. + (Optional) data (str): The data to send with the control message. + + Returns: + bool: True if the control message was successfully sent, False otherwise. + """ + control_msg = json.dumps({"type": msg_type}) + return self.send_raw(control_msg) + + # pylint: enable=unused-argument + + # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements + def send_raw(self, msg: str) -> bool: + """ + Sends a raw/control message over the WebSocket connection. This message must contain a valid JSON object. + + Args: + msg (str): The raw message to send over the WebSocket connection. + + Returns: + bool: True if the message was successfully sent, False otherwise. + """ + self._logger.spam("SpeakWebSocketClient.send_raw ENTER") + + if self._config.is_inspecting_speak(): + try: + _tmp_json = json.loads(msg) + if "type" in _tmp_json: + self._logger.debug( + "Inspecting Message: Sending %s", _tmp_json["type"] + ) + match _tmp_json["type"]: + case SpeakWebSocketMessage.Speak: + inspect_res = self._inspect() + if not inspect_res: + self._logger.error("inspect_res failed") + case SpeakWebSocketMessage.Flush: + with self._lock_flush: + self._last_datagram = None + self._flush_count += 1 + self._logger.debug( + "Increment Flush count: %d", self._flush_count + ) + except Exception as e: # pylint: disable=broad-except + self._logger.error("send_raw() failed - Exception: %s", str(e)) + + try: + if super().send(msg) is False: + self._logger.error("send_raw() failed") + self._logger.spam("SpeakWebSocketClient.send_raw LEAVE") + return False + self._logger.spam("send_raw() succeeded") + self._logger.spam("SpeakWebSocketClient.send_raw LEAVE") + return True + except Exception as e: # pylint: disable=broad-except + self._logger.error("send_raw() failed - Exception: %s", str(e)) + self._logger.spam("SpeakWebSocketClient.send_raw LEAVE") + if self._config.options.get("termination_exception_send") is True: + raise + return False + + # pylint: enable=too-many-return-statements,too-many-branches + + def flush(self) -> bool: + """ + Flushes the current buffer and returns generated audio + """ + self._logger.spam("SpeakWebSocketClient.flush ENTER") + + self._logger.notice("Sending Flush...") + ret = self.send_control(SpeakWebSocketMessage.Flush) + + if not ret: + self._logger.error("flush failed") + self._logger.spam("SpeakWebSocketClient.flush LEAVE") + return False + + self._logger.notice("flush succeeded") + self._logger.spam("SpeakWebSocketClient.flush LEAVE") + + return True + + def clear(self) -> bool: + """ + Clears the current buffer on the server + """ + self._logger.spam("SpeakWebSocketClient.clear ENTER") + + self._logger.notice("Sending Clear...") + ret = self.send_control(SpeakWebSocketMessage.Clear) + + if not ret: + self._logger.error("clear failed") + self._logger.spam("SpeakWebSocketClient.clear LEAVE") + return False + + self._logger.notice("clear succeeded") + self._logger.spam("SpeakWebSocketClient.clear LEAVE") + + return True + + def wait_for_complete(self): + """ + This method will block until the speak is done playing sound. + """ + self._logger.spam("SpeakWebSocketClient.wait_for_complete ENTER") + + if self._speaker is None: + self._logger.error("speaker is None. Return immediately") + raise DeepgramError("Speaker is not initialized") + + self._speaker.wait_for_complete() + self._logger.notice("wait_for_complete succeeded") + self._logger.spam("SpeakWebSocketClient.wait_for_complete LEAVE") + + def _close_message(self) -> bool: + return self.send_control(SpeakWebSocketMessage.Close) + + # closes the WebSocket connection gracefully + def finish(self) -> bool: + """ + Closes the WebSocket connection gracefully. + """ + self._logger.spam("SpeakWebSocketClient.finish ENTER") + + # call parent finish which calls signal_exit + if super().finish() is False: + self._logger.error("ListenWebSocketClient.finish failed") + + if self._speaker is not None and self._speaker_created: + self._speaker.finish() + self._speaker_created = False + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # stop the threads + if self._speaker is not None: + self._logger.verbose("stopping speaker...") + self._speaker.finish() + self._speaker = None + self._logger.notice("speaker stopped") + + if self._flush_thread is not None: + self._logger.verbose("sdtopping _flush_thread...") + self._flush_thread.join() + self._flush_thread = None + self._logger.notice("_flush_thread joined") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("finish succeeded") + self._logger.spam("SpeakWebSocketClient.finish LEAVE") + return True + + def _inspect(self) -> bool: + # auto flush_inspect is generically used to track any messages you might want to snoop on + # place additional logic here to inspect messages of interest + + # for auto flush functionality + # set the last datagram + with self._lock_flush: + self._last_datagram = datetime.now() + self._logger.debug( + "AutoFlush last received: %s", + str(self._last_datagram), + ) + + return True + + +SpeakWebSocketClient = SpeakWSClient diff --git a/deepgram/clients/speak/v1/websocket/options.py b/deepgram/clients/speak/v1/websocket/options.py new file mode 100644 index 0000000000000000000000000000000000000000..50771e0be0fc5a2241170a858cf65e6f395c4a8d --- /dev/null +++ b/deepgram/clients/speak/v1/websocket/options.py @@ -0,0 +1,65 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from io import BufferedReader +from typing import Union, Optional +import logging + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config + +from .....utils import verboselogs +from ....common import BaseResponse + + +@dataclass +class SpeakWSOptions(BaseResponse): + """ + Contains all the options for the SpeakOptions. + + Reference: + https://developers.deepgram.com/reference/transform-text-to-speech-websocket + """ + + model: Optional[str] = field( + default="aura-asteria-en", + metadata=dataclass_config(exclude=lambda f: f is None), + ) + encoding: Optional[str] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + # container: Optional[str] = field( + # default=None, metadata=dataclass_config(exclude=lambda f: f is None) + # ) + sample_rate: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + bit_rate: Optional[int] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + def __str__(self) -> str: + return self.to_json(indent=4) + + def check(self): + """ + Check the SpeakOptions for any missing or invalid values. + """ + logger = verboselogs.VerboseLogger(__name__) + logger.addHandler(logging.StreamHandler()) + prev = logger.level + logger.setLevel(verboselogs.ERROR) + + # no op at the moment + + logger.setLevel(prev) + + return True diff --git a/deepgram/clients/speak/v1/websocket/response.py b/deepgram/clients/speak/v1/websocket/response.py new file mode 100644 index 0000000000000000000000000000000000000000..a5efc4e36fa6060a7e542a3e2cd3cb89ba5a53aa --- /dev/null +++ b/deepgram/clients/speak/v1/websocket/response.py @@ -0,0 +1,58 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + + +from dataclasses import dataclass + +from ....common import ( + BaseResponse, + OpenResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) + + +# Speak Response Types: + + +@dataclass +class MetadataResponse(BaseResponse): + """ + Metadata object + """ + + type: str = "" + request_id: str = "" + + +@dataclass +class FlushedResponse(BaseResponse): + """ + Flushed Message from the Deepgram Platform + """ + + type: str = "" + sequence_id: int = 0 + + +@dataclass +class ClearedResponse(BaseResponse): + """ + Cleared object + """ + + type: str = "" + sequence_id: int = 0 + + +@dataclass +class WarningResponse(BaseResponse): + """ + Warning Message from the Deepgram Platform + """ + + warn_code: str = "" + warn_msg: str = "" + type: str = "" diff --git a/deepgram/clients/speak_router.py b/deepgram/clients/speak_router.py new file mode 100644 index 0000000000000000000000000000000000000000..7290ed48f344adb9570b4f535387f48cd2d24ed9 --- /dev/null +++ b/deepgram/clients/speak_router.py @@ -0,0 +1,171 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from importlib import import_module +import logging +import deprecation # type: ignore + +from .. import __version__ +from .speak.v1.rest.client import SpeakRESTClient +from ..utils import verboselogs +from ..options import DeepgramClientOptions +from .errors import DeepgramModuleError + + +class SpeakRouter: + """ + This class provides a Speak Clients for making requests to the Deepgram API with various configuration options. + + Attributes: + config_options (DeepgramClientOptions): An optional configuration object specifying client options. + + Methods: + rest: (Preferred) Returns a Threaded REST Client instance for interacting with Deepgram's transcription services. + websocket: (Preferred) Returns an Threaded WebSocket Client instance for interacting with Deepgram's prerecorded transcription services. + + asyncrest: Returns an Async REST Client instance for interacting with Deepgram's transcription services. + asyncwebsocket: Returns an Async WebSocket Client instance for interacting with Deepgram's prerecorded transcription services. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + + # when this is removed, remove --disable=W0622 from Makefile + # pylint: disable=unused-argument + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.speak.v1 is deprecated. Use deepgram.speak.rest or deepgram.speak.websocket instead.", + ) + def v(self, version: str = ""): + """ + DEPRECATED: deepgram.speak.v1 is deprecated. Use deepgram.speak.rest or deepgram.speak.websocket instead. + """ + return SpeakRESTClient(self._config) + + # pylint: enable=unused-argument + + @property + def rest(self): + """ + Returns a Threaded REST Client instance for interacting with Deepgram's prerecorded Text-to-Speech services. + """ + return self.Version(self._config, "rest") + + @property + def asyncrest(self): + """ + Returns an Async REST Client instance for interacting with Deepgram's prerecorded Text-to-Speech services. + """ + return self.Version(self._config, "asyncrest") + + @property + def websocket(self): + """ + Returns a Threaded WebSocket Client instance for interacting with Deepgram's Text-to-Speech services. + """ + return self.Version(self._config, "websocket") + + @property + def asyncwebsocket(self): + """ + Returns an Async WebSocket Client instance for interacting with Deepgram's Text-to-Speech services. + """ + return self.Version(self._config, "asyncwebsocket") + + # INTERNAL CLASSES + class Version: + """ + Represents a version of the Deepgram API. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _parent: str + + def __init__(self, config, parent: str): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._parent = parent + + # FUTURE VERSIONING: + # When v2 or v1.1beta1 or etc. This allows easy access to the latest version of the API. + # @property + # def latest(self): + # match self._parent: + # case "live": + # return LiveClient(self._config) + # case "prerecorded": + # return PreRecordedClient(self._config) + # case _: + # raise DeepgramModuleError("Invalid parent") + + def v(self, version: str = ""): + """ + Returns a specific version of the Deepgram API. + """ + self._logger.debug("Version.v ENTER") + self._logger.info("version: %s", version) + if len(version) == 0: + self._logger.error("version is empty") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid module version") + + type = "" + file_name = "" + class_name = "" + match self._parent: + case "websocket": + type = "websocket" + file_name = "client" + class_name = "SpeakWebSocketClient" + case "asyncwebsocket": + type = "websocket" + file_name = "async_client" + class_name = "AsyncSpeakWebSocketClient" + case "rest": + type = "rest" + file_name = "client" + class_name = "SpeakRESTClient" + case "asyncrest": + type = "rest" + file_name = "async_client" + class_name = "AsyncSpeakRESTClient" + case _: + self._logger.error("parent unknown: %s", self._parent) + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid parent type") + + # create class path + path = f"deepgram.clients.speak.v{version}.{type}.{file_name}" + self._logger.info("path: %s", path) + self._logger.info("class_name: %s", class_name) + + # import class + mod = import_module(path) + if mod is None: + self._logger.error("module path is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find package") + + my_class = getattr(mod, class_name) + if my_class is None: + self._logger.error("my_class is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find class") + + # instantiate class + my_class = my_class(self._config) + self._logger.notice("Version.v succeeded") + self._logger.debug("Version.v LEAVE") + return my_class diff --git a/deepgram/errors.py b/deepgram/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..85f2128fc4b73db372f927536d668144eecbe110 --- /dev/null +++ b/deepgram/errors.py @@ -0,0 +1,16 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + + +class DeepgramApiKeyError(Exception): + """ + Base class for exceptions raised for a missing Deepgram API Key. + + Attributes: + message (str): The error message describing the exception. + """ + + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramApiKeyError" diff --git a/deepgram/options.py b/deepgram/options.py new file mode 100644 index 0000000000000000000000000000000000000000..ce43480af9d1154de7de56c17c67d069548a6c35 --- /dev/null +++ b/deepgram/options.py @@ -0,0 +1,245 @@ +# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import sys +import re +import os +from typing import Dict, Optional +import logging +import numbers + +from deepgram import __version__ +from .utils import verboselogs +from .errors import DeepgramApiKeyError + + +class DeepgramClientOptions: # pylint: disable=too-many-instance-attributes + """ + Represents options for configuring a Deepgram client. + + This class allows you to customize various options for interacting with the Deepgram API. + + Attributes: + api_key: (Optional) A Deepgram API key used for authentication. Default uses the `DEEPGRAM_API_KEY` environment variable. + url: (Optional) The URL used to interact with production, On-prem, and other Deepgram environments. Defaults to `api.deepgram.com`. + verbose: (Optional) The logging level for the client. Defaults to `verboselogs.WARNING`. + headers: (Optional) Headers for initializing the client. + options: (Optional) Additional options for initializing the client. + """ + + _logger: verboselogs.VerboseLogger + _inspect_listen: bool = False + _inspect_speak: bool = False + + def __init__( + self, + api_key: str = "", + url: str = "", + verbose: int = verboselogs.WARNING, + headers: Optional[Dict] = None, + options: Optional[Dict] = None, + ): # pylint: disable=too-many-positional-arguments + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + + if api_key is None: + api_key = "" + + self.verbose = verbose + self.api_key = api_key + + if headers is None: + headers = {} + self._update_headers(headers=headers) + + if len(url) == 0: + url = "api.deepgram.com" + self.url = self._get_url(url) + + if options is None: + options = {} + self.options = options + + if self.is_auto_flush_reply_enabled(): + self._inspect_listen = True + if self.is_auto_flush_speak_enabled(): + self._inspect_speak = True + + def set_apikey(self, api_key: str): + """ + set_apikey: Sets the API key for the client. + + Args: + api_key: The Deepgram API key used for authentication. + """ + self.api_key = api_key + self._update_headers() + + def _get_url(self, url) -> str: + if not re.match(r"^https?://", url, re.IGNORECASE): + url = "https://" + url + return url.strip("/") + + def _update_headers(self, headers: Optional[Dict] = None): + self.headers = {} + self.headers["Accept"] = "application/json" + if self.api_key: + self.headers["Authorization"] = f"Token {self.api_key}" + elif "Authorization" in self.headers: + del self.headers["Authorization"] + self.headers[ + "User-Agent" + ] = f"@deepgram/sdk/{__version__} python/{sys.version_info[1]}.{sys.version_info[2]}" + # Overwrite / add any headers that were passed in + if headers: + self.headers.update(headers) + + def is_keep_alive_enabled(self) -> bool: + """ + is_keep_alive_enabled: Returns True if the client is configured to keep the connection alive. + """ + return self.options.get("keepalive", False) or self.options.get( + "keep_alive", False + ) + + def is_auto_flush_reply_enabled(self) -> bool: + """ + is_auto_flush_reply_enabled: Returns True if the client is configured to auto-flush for listen. + """ + auto_flush_reply_delta = float(self.options.get("auto_flush_reply_delta", 0)) + return ( + isinstance(auto_flush_reply_delta, numbers.Number) + and auto_flush_reply_delta > 0 + ) + + def is_auto_flush_speak_enabled(self) -> bool: + """ + is_auto_flush_speak_enabled: Returns True if the client is configured to auto-flush for speak. + """ + auto_flush_speak_delta = float(self.options.get("auto_flush_speak_delta", 0)) + return ( + isinstance(auto_flush_speak_delta, numbers.Number) + and auto_flush_speak_delta > 0 + ) + + def is_inspecting_listen(self) -> bool: + """ + is_inspecting_listen: Returns True if the client is inspecting listen messages. + """ + return self._inspect_listen + + def is_inspecting_speak(self) -> bool: + """ + is_inspecting_speak: Returns True if the client is inspecting speak messages. + """ + return self._inspect_speak + + +class ClientOptionsFromEnv( + DeepgramClientOptions +): # pylint: disable=too-many-branches, too-many-statements + """ + This class extends DeepgramClientOptions and will attempt to use environment variables first before defaults. + """ + + _logger: verboselogs.VerboseLogger + + def __init__( + self, + api_key: str = "", + url: str = "", + verbose: int = verboselogs.WARNING, + headers: Optional[Dict] = None, + options: Optional[Dict] = None, + ): # pylint: disable=too-many-positional-arguments + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(verboselogs.WARNING) # temporary set for setup + + if api_key is None: + api_key = "" + + if api_key == "": + api_key = os.getenv("DEEPGRAM_API_KEY", "") + if api_key == "": + self._logger.critical("Deepgram API KEY is not set") + raise DeepgramApiKeyError("Deepgram API KEY is not set") + + if url == "": + url = os.getenv("DEEPGRAM_HOST", "api.deepgram.com") + self._logger.notice(f"Deepgram host is set to {url}") + + if verbose == verboselogs.WARNING: + _loglevel = os.getenv("DEEPGRAM_LOGGING", "") + if _loglevel != "": + verbose = int(_loglevel) + if isinstance(verbose, str): + match verbose: + case "NOTSET": + self._logger.notice("Logging level is set to NOTSET") + verbose = verboselogs.NOTSET + case "SPAM": + self._logger.notice("Logging level is set to SPAM") + verbose = verboselogs.SPAM + case "DEBUG": + self._logger.notice("Logging level is set to DEBUG") + verbose = verboselogs.DEBUG + case "VERBOSE": + self._logger.notice("Logging level is set to VERBOSE") + verbose = verboselogs.VERBOSE + case "NOTICE": + self._logger.notice("Logging level is set to NOTICE") + verbose = verboselogs.NOTICE + case "WARNING": + self._logger.notice("Logging level is set to WARNING") + verbose = verboselogs.WARNING + case "SUCCESS": + self._logger.notice("Logging level is set to SUCCESS") + verbose = verboselogs.SUCCESS + case "ERROR": + self._logger.notice("Logging level is set to ERROR") + verbose = verboselogs.ERROR + case "CRITICAL": + self._logger.notice("Logging level is set to CRITICAL") + verbose = verboselogs.CRITICAL + case _: + self._logger.notice("Logging level is set to WARNING") + verbose = verboselogs.WARNING + self._logger.notice(f"Logging level is set to {verbose}") + + if headers is None: + headers = {} + for x in range(0, 20): + header = os.getenv(f"DEEPGRAM_HEADER_{x}", None) + if header is not None: + headers[header] = os.getenv(f"DEEPGRAM_HEADER_VALUE_{x}", None) + self._logger.debug( + "Deepgram header %s is set with value %s", + header, + headers[header], + ) + else: + break + if len(headers) == 0: + self._logger.notice("Deepgram headers are not set") + headers = None + + if options is None: + options = {} + for x in range(0, 20): + param = os.getenv(f"DEEPGRAM_PARAM_{x}", None) + if param is not None: + options[param] = os.getenv(f"DEEPGRAM_PARAM_VALUE_{x}", None) + self._logger.debug( + "Deepgram option %s is set with value %s", param, options[param] + ) + else: + break + if len(options) == 0: + self._logger.notice("Deepgram options are not set") + options = None + + super().__init__( + api_key=api_key, url=url, verbose=verbose, headers=headers, options=options + ) diff --git a/deepgram/utils/__init__.py b/deepgram/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..21343d9d4071442f00202b7e6ed8bfe9c5694a64 --- /dev/null +++ b/deepgram/utils/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import logging +from .verboselogs import VerboseLogger +from .verboselogs import ( + NOTICE, + SPAM, + SUCCESS, + VERBOSE, + WARNING, + ERROR, + FATAL, + CRITICAL, + INFO, + DEBUG, + NOTSET, +) diff --git a/deepgram/utils/__pycache__/__init__.cpython-313.pyc b/deepgram/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b51f1266842e820e0f534a0d598cc49977f2eba8 Binary files /dev/null and b/deepgram/utils/__pycache__/__init__.cpython-313.pyc differ diff --git a/deepgram/utils/verboselogs/__init__.py b/deepgram/utils/verboselogs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c7d39326e1438b0e4abfaa94894f67ac0a1d62df --- /dev/null +++ b/deepgram/utils/verboselogs/__init__.py @@ -0,0 +1,168 @@ +# Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +""" +Custom log levels for Python's :mod:`logging` module. + +The :mod:`verboselogs` module defines the :data:`NOTICE`, :data:`SPAM`, +:data:`SUCCESS` and :data:`VERBOSE` constants, the :class:`VerboseLogger` class +and the :func:`add_log_level()` and :func:`install()` functions. + +At import time :func:`add_log_level()` is used to register the custom log +levels :data:`NOTICE`, :data:`SPAM`, :data:`SUCCESS` and :data:`VERBOSE` with +Python's :mod:`logging` module. +""" + +import logging + +__version__ = "1.8" +"""Semi-standard module versioning.""" + +NOTICE = 25 +""" +The numeric value of the 'notice' log level (a number). + +The value of :data:`NOTICE` positions the notice log level between the +:data:`~verboselogs.WARNING` and :data:`~verboselogs.INFO` levels. + +:see also: The :func:`~VerboseLogger.notice()` method of the + :class:`VerboseLogger` class. +""" + +SPAM = 5 +""" +The numeric value of the 'spam' log level (a number). + +The value of :data:`SPAM` positions the spam log level between the +:data:`~verboselogs.DEBUG` and :data:`~verboselogs.NOTSET` levels. + +:see also: The :func:`~VerboseLogger.spam()` method of the + :class:`VerboseLogger` class. +""" + +SUCCESS = 35 +""" +The numeric value of the 'success' log level (a number). + +The value of :data:`SUCCESS` positions the success log level between the +:data:`~verboselogs.WARNING` and :data:`~verboselogs.ERROR` levels. + +:see also: The :func:`~VerboseLogger.success()` method of the + :class:`VerboseLogger` class. +""" + +VERBOSE = 15 +""" +The numeric value of the 'verbose' log level (a number). + +The value of :data:`VERBOSE` positions the verbose log level between the +:data:`~verboselogs.INFO` and :data:`~verboselogs.DEBUG` levels. + +:see also: The :func:`~VerboseLogger.verbose()` method of the + :class:`VerboseLogger` class. +""" + + +def install(): + """ + Make :class:`VerboseLogger` the default logger class. + + The :func:`install()` function uses :func:`~logging.setLoggerClass()` to + configure :class:`VerboseLogger` as the default class for all loggers + created by :func:`logging.getLogger()` after :func:`install()` has been + called. Here's how it works: + + .. code-block:: python + + import logging + import verboselogs + + verboselogs.install() + logger = logging.getLogger(__name__) # will be a VerboseLogger instance + """ + logging.setLoggerClass(VerboseLogger) + + +def add_log_level(value, name): + """ + Add a new log level to the :mod:`logging` module. + + :param value: The log level's number (an integer). + :param name: The name for the log level (a string). + """ + logging.addLevelName(value, name) + setattr(logging, name, value) + + +WARNING = logging.WARNING +ERROR = logging.ERROR +FATAL = logging.FATAL +CRITICAL = logging.CRITICAL +INFO = logging.INFO +DEBUG = logging.DEBUG +NOTSET = logging.NOTSET + +# Define the NOTICE log level. +add_log_level(NOTICE, "NOTICE") + +# Define the SPAM log level. +add_log_level(SPAM, "SPAM") + +# Define the SUCCESS log level. +add_log_level(SUCCESS, "SUCCESS") + +# Define the VERBOSE log level. +add_log_level(VERBOSE, "VERBOSE") + + +class VerboseLogger(logging.Logger): + """ + Custom logger class to support the additional logging levels. + + This subclass of :class:`verboselogs.Logger` adds support for the additional + logging methods :func:`notice()`, :func:`spam()`, :func:`success()` and + :func:`verbose()`. + + You can use :func:`verboselogs.install()` to make :class:`VerboseLogger` + the default logger class. + """ + + def __init__(self, *args, **kw): + """ + Initialize a :class:`VerboseLogger` object. + + :param args: Refer to the superclass (:class:`verboselogs.Logger`). + :param kw: Refer to the superclass (:class:`verboselogs.Logger`). + + This method first initializes the superclass and then it sets the root + logger as the parent of this logger. + + The function :func:`logging.getLogger()` is normally responsible for + defining the hierarchy of logger objects however because verbose + loggers can be created by calling the :class:`VerboseLogger` + constructor, we're responsible for defining the parent relationship + ourselves. + """ + logging.Logger.__init__(self, *args, **kw) + self.parent = logging.getLogger() + + def notice(self, msg, *args, **kw): + """Log a message with level :data:`NOTICE`. The arguments are interpreted as for :func:`logging.debug()`.""" + if self.isEnabledFor(NOTICE): + self._log(NOTICE, msg, args, **kw) + + def spam(self, msg, *args, **kw): + """Log a message with level :data:`SPAM`. The arguments are interpreted as for :func:`logging.debug()`.""" + if self.isEnabledFor(SPAM): + self._log(SPAM, msg, args, **kw) + + def success(self, msg, *args, **kw): + """Log a message with level :data:`SUCCESS`. The arguments are interpreted as for :func:`logging.debug()`.""" + if self.isEnabledFor(SUCCESS): + self._log(SUCCESS, msg, args, **kw) + + def verbose(self, msg, *args, **kw): + """Log a message with level :data:`VERBOSE`. The arguments are interpreted as for :func:`logging.debug()`.""" + if self.isEnabledFor(VERBOSE): + self._log(VERBOSE, msg, args, **kw) diff --git a/deepgram/utils/verboselogs/__pycache__/__init__.cpython-313.pyc b/deepgram/utils/verboselogs/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..058c52fec754142689985df74ebd44869861d40e Binary files /dev/null and b/deepgram/utils/verboselogs/__pycache__/__init__.cpython-313.pyc differ