Handles the main WebSocket connection for real-time voice chat. Accepts a connection, sets up connection-specific state via `TranscriptionCallbacks`, initializes audio/message queues, and creates asyncio tasks for handling incoming data, audio processing, outgoing text messages, an
(ws: WebSocket)
| 872 | # -------------------------------------------------------------------- |
| 873 | @app.websocket("/ws") |
| 874 | async def websocket_endpoint(ws: WebSocket): |
| 875 | """ |
| 876 | Handles the main WebSocket connection for real-time voice chat. |
| 877 | |
| 878 | Accepts a connection, sets up connection-specific state via `TranscriptionCallbacks`, |
| 879 | initializes audio/message queues, and creates asyncio tasks for handling |
| 880 | incoming data, audio processing, outgoing text messages, and outgoing TTS chunks. |
| 881 | Manages the lifecycle of these tasks and cleans up on disconnect. |
| 882 | |
| 883 | Args: |
| 884 | ws: The WebSocket connection instance provided by FastAPI. |
| 885 | """ |
| 886 | await ws.accept() |
| 887 | logger.info("🖥️✅ Client connected via WebSocket.") |
| 888 | |
| 889 | message_queue = asyncio.Queue() |
| 890 | audio_chunks = asyncio.Queue() |
| 891 | |
| 892 | # Set up callback manager - THIS NOW HOLDS THE CONNECTION-SPECIFIC STATE |
| 893 | callbacks = TranscriptionCallbacks(app, message_queue) |
| 894 | |
| 895 | # Assign callbacks to the AudioInputProcessor (global component) |
| 896 | # These methods within callbacks will now operate on its *instance* state |
| 897 | app.state.AudioInputProcessor.realtime_callback = callbacks.on_partial |
| 898 | app.state.AudioInputProcessor.transcriber.potential_sentence_end = callbacks.on_potential_sentence |
| 899 | app.state.AudioInputProcessor.transcriber.on_tts_allowed_to_synthesize = callbacks.on_tts_allowed_to_synthesize |
| 900 | app.state.AudioInputProcessor.transcriber.potential_full_transcription_callback = callbacks.on_potential_final |
| 901 | app.state.AudioInputProcessor.transcriber.potential_full_transcription_abort_callback = callbacks.on_potential_abort |
| 902 | app.state.AudioInputProcessor.transcriber.full_transcription_callback = callbacks.on_final |
| 903 | app.state.AudioInputProcessor.transcriber.before_final_sentence = callbacks.on_before_final |
| 904 | app.state.AudioInputProcessor.recording_start_callback = callbacks.on_recording_start |
| 905 | app.state.AudioInputProcessor.silence_active_callback = callbacks.on_silence_active |
| 906 | |
| 907 | # Assign callback to the SpeechPipelineManager (global component) |
| 908 | app.state.SpeechPipelineManager.on_partial_assistant_text = callbacks.on_partial_assistant_text |
| 909 | |
| 910 | # Create tasks for handling different responsibilities |
| 911 | # Pass the 'callbacks' instance to tasks that need connection-specific state |
| 912 | tasks = [ |
| 913 | asyncio.create_task(process_incoming_data(ws, app, audio_chunks, callbacks)), # Pass callbacks |
| 914 | asyncio.create_task(app.state.AudioInputProcessor.process_chunk_queue(audio_chunks)), |
| 915 | asyncio.create_task(send_text_messages(ws, message_queue)), |
| 916 | asyncio.create_task(send_tts_chunks(app, message_queue, callbacks)), # Pass callbacks |
| 917 | ] |
| 918 | |
| 919 | try: |
| 920 | # Wait for any task to complete (e.g., client disconnect) |
| 921 | done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) |
| 922 | for task in pending: |
| 923 | if not task.done(): |
| 924 | task.cancel() |
| 925 | # Await cancelled tasks to let them clean up if needed |
| 926 | await asyncio.gather(*pending, return_exceptions=True) |
| 927 | except Exception as e: |
| 928 | logger.error(f"🖥️💥 {Colors.apply('ERROR').red} in WebSocket session: {repr(e)}") |
| 929 | finally: |
| 930 | logger.info("🖥️🧹 Cleaning up WebSocket tasks...") |
| 931 | for task in tasks: |
nothing calls this directly
no test coverage detected