audio_recorder_client.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. log_outgoing_chunks = False
  2. debug_mode = False
  3. from typing import Iterable, List, Optional, Union
  4. from urllib.parse import urlparse
  5. from datetime import datetime
  6. import subprocess
  7. import websocket
  8. import threading
  9. import platform
  10. import logging
  11. import pyaudio
  12. import socket
  13. import struct
  14. import signal
  15. import json
  16. import time
  17. import sys
  18. import os
  19. DEFAULT_CONTROL_URL = "ws://127.0.0.1:8011"
  20. DEFAULT_DATA_URL = "ws://127.0.0.1:8012"
  21. INIT_MODEL_TRANSCRIPTION = "tiny"
  22. INIT_MODEL_TRANSCRIPTION_REALTIME = "tiny"
  23. INIT_REALTIME_PROCESSING_PAUSE = 0.2
  24. INIT_SILERO_SENSITIVITY = 0.4
  25. INIT_WEBRTC_SENSITIVITY = 3
  26. INIT_POST_SPEECH_SILENCE_DURATION = 0.6
  27. INIT_MIN_LENGTH_OF_RECORDING = 0.5
  28. INIT_MIN_GAP_BETWEEN_RECORDINGS = 0
  29. INIT_PRE_RECORDING_BUFFER_DURATION = 1.0
  30. ALLOWED_LATENCY_LIMIT = 100
  31. CHUNK = 1024
  32. FORMAT = pyaudio.paInt16
  33. CHANNELS = 1
  34. SAMPLE_RATE = 16000
  35. BUFFER_SIZE = 512
  36. INIT_HANDLE_BUFFER_OVERFLOW = False
  37. if platform.system() != 'Darwin':
  38. INIT_HANDLE_BUFFER_OVERFLOW = True
  39. # Define ANSI color codes for terminal output
  40. class bcolors:
  41. HEADER = '\033[95m' # Magenta
  42. OKBLUE = '\033[94m' # Blue
  43. OKCYAN = '\033[96m' # Cyan
  44. OKGREEN = '\033[92m' # Green
  45. WARNING = '\033[93m' # Yellow
  46. FAIL = '\033[91m' # Red
  47. ENDC = '\033[0m' # Reset to default
  48. BOLD = '\033[1m'
  49. UNDERLINE = '\033[4m'
  50. class AudioToTextRecorderClient:
  51. """
  52. A class responsible for capturing audio from the microphone, detecting
  53. voice activity, and then transcribing the captured audio using the
  54. `faster_whisper` model.
  55. """
  56. def __init__(self,
  57. model: str = INIT_MODEL_TRANSCRIPTION,
  58. language: str = "",
  59. compute_type: str = "default",
  60. input_device_index: int = None,
  61. gpu_device_index: Union[int, List[int]] = 0,
  62. device: str = "cuda",
  63. on_recording_start=None,
  64. on_recording_stop=None,
  65. on_transcription_start=None,
  66. ensure_sentence_starting_uppercase=True,
  67. ensure_sentence_ends_with_period=True,
  68. use_microphone=True,
  69. spinner=True,
  70. level=logging.WARNING,
  71. # Realtime transcription parameters
  72. enable_realtime_transcription=False,
  73. use_main_model_for_realtime=False,
  74. realtime_model_type=INIT_MODEL_TRANSCRIPTION_REALTIME,
  75. realtime_processing_pause=INIT_REALTIME_PROCESSING_PAUSE,
  76. on_realtime_transcription_update=None,
  77. on_realtime_transcription_stabilized=None,
  78. # Voice activation parameters
  79. silero_sensitivity: float = INIT_SILERO_SENSITIVITY,
  80. silero_use_onnx: bool = False,
  81. silero_deactivity_detection: bool = False,
  82. webrtc_sensitivity: int = INIT_WEBRTC_SENSITIVITY,
  83. post_speech_silence_duration: float = (
  84. INIT_POST_SPEECH_SILENCE_DURATION
  85. ),
  86. min_length_of_recording: float = (
  87. INIT_MIN_LENGTH_OF_RECORDING
  88. ),
  89. min_gap_between_recordings: float = (
  90. INIT_MIN_GAP_BETWEEN_RECORDINGS
  91. ),
  92. pre_recording_buffer_duration: float = (
  93. INIT_PRE_RECORDING_BUFFER_DURATION
  94. ),
  95. on_vad_detect_start=None,
  96. on_vad_detect_stop=None,
  97. on_recorded_chunk=None,
  98. debug_mode=False,
  99. handle_buffer_overflow: bool = INIT_HANDLE_BUFFER_OVERFLOW,
  100. beam_size: int = 5,
  101. beam_size_realtime: int = 3,
  102. buffer_size: int = BUFFER_SIZE,
  103. sample_rate: int = SAMPLE_RATE,
  104. initial_prompt: Optional[Union[str, Iterable[int]]] = None,
  105. suppress_tokens: Optional[List[int]] = [-1],
  106. print_transcription_time: bool = False,
  107. early_transcription_on_silence: int = 0,
  108. allowed_latency_limit: int = ALLOWED_LATENCY_LIMIT,
  109. no_log_file: bool = False,
  110. use_extended_logging: bool = False,
  111. # Server urls
  112. control_url: str = DEFAULT_CONTROL_URL,
  113. data_url: str = DEFAULT_DATA_URL,
  114. autostart_server: bool = True,
  115. ):
  116. # Set instance variables from constructor parameters
  117. self.model = model
  118. self.language = language
  119. self.compute_type = compute_type
  120. self.input_device_index = input_device_index
  121. self.gpu_device_index = gpu_device_index
  122. self.device = device
  123. self.on_recording_start = on_recording_start
  124. self.on_recording_stop = on_recording_stop
  125. self.on_transcription_start = on_transcription_start
  126. self.ensure_sentence_starting_uppercase = ensure_sentence_starting_uppercase
  127. self.ensure_sentence_ends_with_period = ensure_sentence_ends_with_period
  128. self.use_microphone = use_microphone
  129. self.spinner = spinner
  130. self.level = level
  131. # Real-time transcription parameters
  132. self.enable_realtime_transcription = enable_realtime_transcription
  133. self.use_main_model_for_realtime = use_main_model_for_realtime
  134. self.realtime_model_type = realtime_model_type
  135. self.realtime_processing_pause = realtime_processing_pause
  136. self.on_realtime_transcription_update = on_realtime_transcription_update
  137. self.on_realtime_transcription_stabilized = on_realtime_transcription_stabilized
  138. # Voice activation parameters
  139. self.silero_sensitivity = silero_sensitivity
  140. self.silero_use_onnx = silero_use_onnx
  141. self.silero_deactivity_detection = silero_deactivity_detection
  142. self.webrtc_sensitivity = webrtc_sensitivity
  143. self.post_speech_silence_duration = post_speech_silence_duration
  144. self.min_length_of_recording = min_length_of_recording
  145. self.min_gap_between_recordings = min_gap_between_recordings
  146. self.pre_recording_buffer_duration = pre_recording_buffer_duration
  147. self.on_vad_detect_start = on_vad_detect_start
  148. self.on_vad_detect_stop = on_vad_detect_stop
  149. self.on_recorded_chunk = on_recorded_chunk
  150. self.debug_mode = debug_mode
  151. self.handle_buffer_overflow = handle_buffer_overflow
  152. self.beam_size = beam_size
  153. self.beam_size_realtime = beam_size_realtime
  154. self.buffer_size = buffer_size
  155. self.sample_rate = sample_rate
  156. self.initial_prompt = initial_prompt
  157. self.suppress_tokens = suppress_tokens
  158. self.print_transcription_time = print_transcription_time
  159. self.early_transcription_on_silence = early_transcription_on_silence
  160. self.allowed_latency_limit = allowed_latency_limit
  161. self.no_log_file = no_log_file
  162. self.use_extended_logging = use_extended_logging
  163. # Server URLs
  164. self.control_url = control_url
  165. self.data_url = data_url
  166. self.autostart_server = autostart_server
  167. # Instance variables
  168. self.muted = False
  169. self.recording_thread = None
  170. self.is_running = True
  171. self.connection_established = threading.Event()
  172. self.recording_start = threading.Event()
  173. self.final_text_ready = threading.Event()
  174. self.realtime_text = ""
  175. self.final_text = ""
  176. self.request_counter = 0
  177. self.pending_requests = {} # Map from request_id to threading.Event and value
  178. if self.debug_mode:
  179. print("Checking STT server")
  180. if not self.connect():
  181. print("Failed to connect to the server.", file=sys.stderr)
  182. else:
  183. if self.debug_mode:
  184. print("STT server is running and connected.")
  185. if self.use_microphone:
  186. self.start_recording()
  187. def text(self, on_transcription_finished=None):
  188. self.realtime_text = ""
  189. self.submitted_realtime_text = ""
  190. self.final_text = ""
  191. self.final_text_ready.clear()
  192. self.recording_start.set()
  193. try:
  194. total_wait_time = 0
  195. wait_interval = 0.02 # Wait in small intervals, e.g., 100ms
  196. max_wait_time = 60 # Timeout after 60 seconds
  197. while total_wait_time < max_wait_time:
  198. if self.final_text_ready.wait(timeout=wait_interval):
  199. break # Break if transcription is ready
  200. # if not self.realtime_text == self.submitted_realtime_text:
  201. # if self.on_realtime_transcription_update:
  202. # self.on_realtime_transcription_update(self.realtime_text)
  203. # self.submitted_realtime_text = self.realtime_text
  204. total_wait_time += wait_interval
  205. # Check if a manual interrupt has occurred
  206. if total_wait_time >= max_wait_time:
  207. if self.debug_mode:
  208. print("Timeout while waiting for text from the server.")
  209. self.recording_start.clear()
  210. if on_transcription_finished:
  211. threading.Thread(target=on_transcription_finished, args=("",)).start()
  212. return ""
  213. self.recording_start.clear()
  214. if on_transcription_finished:
  215. threading.Thread(target=on_transcription_finished, args=(self.final_text,)).start()
  216. return self.final_text
  217. except KeyboardInterrupt:
  218. if self.debug_mode:
  219. print("KeyboardInterrupt in record_and_send_audio, exiting...")
  220. raise KeyboardInterrupt
  221. except Exception as e:
  222. print(f"Error in AudioToTextRecorderClient.text(): {e}")
  223. return ""
  224. def feed_audio(self, chunk, original_sample_rate=16000):
  225. metadata = {"sampleRate": original_sample_rate}
  226. metadata_json = json.dumps(metadata)
  227. metadata_length = len(metadata_json)
  228. message = struct.pack('<I', metadata_length) + metadata_json.encode('utf-8') + chunk
  229. if self.is_running:
  230. self.data_ws.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
  231. def set_microphone(self, microphone_on=True):
  232. """
  233. Set the microphone on or off.
  234. """
  235. self.muted = not microphone_on
  236. def abort(self):
  237. self.call_method("abort")
  238. def clear_audio_queue(self):
  239. self.call_method("clear_audio_queue")
  240. def stop(self):
  241. self.call_method("stop")
  242. def connect(self):
  243. if not self.ensure_server_running():
  244. print("Cannot start STT server. Exiting.")
  245. return False
  246. try:
  247. # Connect to control WebSocket
  248. self.control_ws = websocket.WebSocketApp(self.control_url,
  249. on_message=self.on_control_message,
  250. on_error=self.on_error,
  251. on_close=self.on_close,
  252. on_open=self.on_control_open)
  253. self.control_ws_thread = threading.Thread(target=self.control_ws.run_forever)
  254. self.control_ws_thread.daemon = False
  255. self.control_ws_thread.start()
  256. # Connect to data WebSocket
  257. self.data_ws = websocket.WebSocketApp(self.data_url,
  258. on_message=self.on_data_message,
  259. on_error=self.on_error,
  260. on_close=self.on_close,
  261. on_open=self.on_data_open)
  262. self.data_ws_thread = threading.Thread(target=self.data_ws.run_forever)
  263. self.data_ws_thread.daemon = False
  264. self.data_ws_thread.start()
  265. # Wait for the connections to be established
  266. if not self.connection_established.wait(timeout=10):
  267. print("Timeout while connecting to the server.")
  268. return False
  269. if self.debug_mode:
  270. print("WebSocket connections established successfully.")
  271. return True
  272. except Exception as e:
  273. print(f"Error while connecting to the server: {e}")
  274. return False
  275. def start_server(self):
  276. args = ['stt-server']
  277. # Map constructor parameters to server arguments
  278. if self.model:
  279. args += ['--model', self.model]
  280. if self.realtime_model_type:
  281. args += ['--realtime_model_type', self.realtime_model_type]
  282. if self.language:
  283. args += ['--language', self.language]
  284. if self.silero_sensitivity is not None:
  285. args += ['--silero_sensitivity', str(self.silero_sensitivity)]
  286. if self.silero_use_onnx:
  287. args.append('--silero_use_onnx') # flag, no need for True/False
  288. if self.webrtc_sensitivity is not None:
  289. args += ['--webrtc_sensitivity', str(self.webrtc_sensitivity)]
  290. if self.min_length_of_recording is not None:
  291. args += ['--min_length_of_recording', str(self.min_length_of_recording)]
  292. if self.min_gap_between_recordings is not None:
  293. args += ['--min_gap_between_recordings', str(self.min_gap_between_recordings)]
  294. if self.realtime_processing_pause is not None:
  295. args += ['--realtime_processing_pause', str(self.realtime_processing_pause)]
  296. if self.early_transcription_on_silence is not None:
  297. args += ['--early_transcription_on_silence', str(self.early_transcription_on_silence)]
  298. if self.silero_deactivity_detection:
  299. args.append('--silero_deactivity_detection') # flag, no need for True/False
  300. if self.beam_size is not None:
  301. args += ['--beam_size', str(self.beam_size)]
  302. if self.beam_size_realtime is not None:
  303. args += ['--beam_size_realtime', str(self.beam_size_realtime)]
  304. if self.use_main_model_for_realtime:
  305. args.append('--use_main_model_for_realtime') # flag, no need for True/False
  306. if self.use_extended_logging:
  307. args.append('--use_extended_logging') # flag, no need for True/False
  308. if self.control_url:
  309. parsed_control_url = urlparse(self.control_url)
  310. if parsed_control_url.port:
  311. args += ['--control_port', str(parsed_control_url.port)]
  312. if self.data_url:
  313. parsed_data_url = urlparse(self.data_url)
  314. if parsed_data_url.port:
  315. args += ['--data_port', str(parsed_data_url.port)]
  316. if self.initial_prompt:
  317. sanitized_prompt = self.initial_prompt.replace("\n", "\\n")
  318. args += ['--initial_prompt', sanitized_prompt]
  319. # Start the subprocess with the mapped arguments
  320. if os.name == 'nt': # Windows
  321. cmd = 'start /min cmd /c ' + subprocess.list2cmdline(args)
  322. if debug_mode:
  323. print(f"Opening server with cli command: {cmd}")
  324. subprocess.Popen(cmd, shell=True)
  325. else: # Unix-like systems
  326. subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
  327. print("STT server start command issued. Please wait a moment for it to initialize.", file=sys.stderr)
  328. def is_server_running(self):
  329. parsed_url = urlparse(self.control_url)
  330. host = parsed_url.hostname
  331. port = parsed_url.port or 80
  332. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  333. return s.connect_ex((host, port)) == 0
  334. def ensure_server_running(self):
  335. if not self.is_server_running():
  336. if self.debug_mode:
  337. print("STT server is not running.", file=sys.stderr)
  338. if self.autostart_server or self.ask_to_start_server():
  339. self.start_server()
  340. if self.debug_mode:
  341. print("Waiting for STT server to start...", file=sys.stderr)
  342. for _ in range(20): # Wait up to 20 seconds
  343. if self.is_server_running():
  344. if self.debug_mode:
  345. print("STT server started successfully.", file=sys.stderr)
  346. time.sleep(2) # Give the server a moment to fully initialize
  347. return True
  348. time.sleep(1)
  349. print("Failed to start STT server.", file=sys.stderr)
  350. return False
  351. else:
  352. print("STT server is required. Please start it manually.", file=sys.stderr)
  353. return False
  354. return True
  355. def start_recording(self):
  356. self.recording_thread = threading.Thread(target=self.record_and_send_audio)
  357. self.recording_thread.daemon = False
  358. self.recording_thread.start()
  359. def setup_audio(self):
  360. try:
  361. self.audio_interface = pyaudio.PyAudio()
  362. self.input_device_index = None
  363. try:
  364. default_device = self.audio_interface.get_default_input_device_info()
  365. self.input_device_index = default_device['index']
  366. except OSError as e:
  367. print(f"No default input device found: {e}")
  368. return False
  369. self.device_sample_rate = 16000 # Try 16000 Hz first
  370. try:
  371. self.stream = self.audio_interface.open(
  372. format=FORMAT,
  373. channels=CHANNELS,
  374. rate=self.device_sample_rate,
  375. input=True,
  376. frames_per_buffer=CHUNK,
  377. input_device_index=self.input_device_index,
  378. )
  379. if self.debug_mode:
  380. print(f"Audio recording initialized successfully at {self.device_sample_rate} Hz")
  381. return True
  382. except Exception as e:
  383. print(f"Failed to initialize audio stream at {self.device_sample_rate} Hz: {e}")
  384. return False
  385. except Exception as e:
  386. print(f"Error initializing audio recording: {e}")
  387. if self.audio_interface:
  388. self.audio_interface.terminate()
  389. return False
  390. def record_and_send_audio(self):
  391. try:
  392. if not self.setup_audio():
  393. raise Exception("Failed to set up audio recording.")
  394. if self.debug_mode:
  395. print("Recording and sending audio...")
  396. while self.is_running:
  397. if self.muted:
  398. time.sleep(0.01)
  399. continue
  400. try:
  401. audio_data = self.stream.read(CHUNK)
  402. if self.on_recorded_chunk:
  403. self.on_recorded_chunk(audio_data)
  404. if self.muted:
  405. continue
  406. if self.recording_start.is_set():
  407. metadata = {"sampleRate": self.device_sample_rate}
  408. metadata_json = json.dumps(metadata)
  409. metadata_length = len(metadata_json)
  410. message = struct.pack('<I', metadata_length) + metadata_json.encode('utf-8') + audio_data
  411. if self.is_running:
  412. if log_outgoing_chunks:
  413. print(".", flush=True, end='')
  414. self.data_ws.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
  415. except KeyboardInterrupt: # handle manual interruption (Ctrl+C)
  416. if self.debug_mode:
  417. print("KeyboardInterrupt in record_and_send_audio, exiting...")
  418. break
  419. except Exception as e:
  420. print(f"Error sending audio data: {e}")
  421. break # Exit the recording loop
  422. except Exception as e:
  423. print(f"Error in record_and_send_audio: {e}")
  424. finally:
  425. self.cleanup_audio()
  426. def cleanup_audio(self):
  427. try:
  428. if self.stream:
  429. self.stream.stop_stream()
  430. self.stream.close()
  431. self.stream = None
  432. if self.audio_interface:
  433. self.audio_interface.terminate()
  434. self.audio_interface = None
  435. except Exception as e:
  436. print(f"Error cleaning up audio resources: {e}")
  437. def on_control_message(self, ws, message):
  438. try:
  439. data = json.loads(message)
  440. # Handle server response with status
  441. if 'status' in data:
  442. if data['status'] == 'success':
  443. if 'parameter' in data and 'value' in data:
  444. request_id = data.get('request_id')
  445. if request_id is not None and request_id in self.pending_requests:
  446. if self.debug_mode:
  447. print(f"Parameter {data['parameter']} = {data['value']}")
  448. self.pending_requests[request_id]['value'] = data['value']
  449. self.pending_requests[request_id]['event'].set()
  450. elif data['status'] == 'error':
  451. print(f"Server Error: {data.get('message', '')}")
  452. else:
  453. print(f"Unknown control message format: {data}")
  454. except json.JSONDecodeError:
  455. print(f"Received non-JSON control message: {message}")
  456. except Exception as e:
  457. print(f"Error processing control message: {e}")
  458. # Handle real-time transcription and full sentence updates
  459. def on_data_message(self, ws, message):
  460. try:
  461. data = json.loads(message)
  462. # Handle real-time transcription updates
  463. if data.get('type') == 'realtime':
  464. if data['text'] != self.realtime_text:
  465. self.realtime_text = data['text']
  466. timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
  467. print(f"Realtime text [{timestamp}]: {bcolors.OKCYAN}{self.realtime_text}{bcolors.ENDC}")
  468. if self.on_realtime_transcription_update:
  469. # Call the callback in a new thread to avoid blocking
  470. threading.Thread(
  471. target=self.on_realtime_transcription_update,
  472. args=(self.realtime_text,)
  473. ).start()
  474. # Handle full sentences
  475. elif data.get('type') == 'fullSentence':
  476. self.final_text = data['text']
  477. self.final_text_ready.set()
  478. elif data.get('type') == 'recording_start':
  479. if self.on_recording_start:
  480. self.on_recording_start()
  481. elif data.get('type') == 'recording_stop':
  482. if self.on_recording_stop:
  483. self.on_recording_stop()
  484. elif data.get('type') == 'transcription_start':
  485. if self.on_transcription_start:
  486. self.on_transcription_start()
  487. elif data.get('type') == 'vad_detect_start':
  488. if self.on_vad_detect_start:
  489. self.on_vad_detect_start()
  490. elif data.get('type') == 'vad_detect_stop':
  491. if self.on_vad_detect_stop:
  492. self.on_vad_detect_stop()
  493. else:
  494. print(f"Unknown data message format: {data}")
  495. except json.JSONDecodeError:
  496. print(f"Received non-JSON data message: {message}")
  497. except Exception as e:
  498. print(f"Error processing data message: {e}")
  499. def on_error(self, ws, error):
  500. print(f"WebSocket error: {error}")
  501. def on_close(self, ws, close_status_code, close_msg):
  502. if self.debug_mode:
  503. if ws == self.data_ws:
  504. print(f"Data WebSocket connection closed: {close_status_code} - {close_msg}")
  505. elif ws == self.control_ws:
  506. print(f"Control WebSocket connection closed: {close_status_code} - {close_msg}")
  507. self.is_running = False
  508. def on_control_open(self, ws):
  509. if self.debug_mode:
  510. print("Control WebSocket connection opened.")
  511. self.connection_established.set()
  512. def on_data_open(self, ws):
  513. if self.debug_mode:
  514. print("Data WebSocket connection opened.")
  515. def set_parameter(self, parameter, value):
  516. command = {
  517. "command": "set_parameter",
  518. "parameter": parameter,
  519. "value": value
  520. }
  521. self.control_ws.send(json.dumps(command))
  522. def get_parameter(self, parameter):
  523. # Generate a unique request_id
  524. request_id = self.request_counter
  525. self.request_counter += 1
  526. # Prepare the command with the request_id
  527. command = {
  528. "command": "get_parameter",
  529. "parameter": parameter,
  530. "request_id": request_id
  531. }
  532. # Create an event to wait for the response
  533. event = threading.Event()
  534. self.pending_requests[request_id] = {'event': event, 'value': None}
  535. # Send the command to the server
  536. self.control_ws.send(json.dumps(command))
  537. # Wait for the response or timeout after 5 seconds
  538. if event.wait(timeout=5):
  539. value = self.pending_requests[request_id]['value']
  540. # Clean up the pending request
  541. del self.pending_requests[request_id]
  542. return value
  543. else:
  544. print(f"Timeout waiting for get_parameter {parameter}")
  545. # Clean up the pending request
  546. del self.pending_requests[request_id]
  547. return None
  548. def call_method(self, method, args=None, kwargs=None):
  549. command = {
  550. "command": "call_method",
  551. "method": method,
  552. "args": args or [],
  553. "kwargs": kwargs or {}
  554. }
  555. self.control_ws.send(json.dumps(command))
  556. def shutdown(self):
  557. self.is_running = False
  558. #self.stop_event.set()
  559. if self.control_ws:
  560. self.control_ws.close()
  561. if self.data_ws:
  562. self.data_ws.close()
  563. # Join threads to ensure they finish before exiting
  564. if self.control_ws_thread:
  565. self.control_ws_thread.join()
  566. if self.data_ws_thread:
  567. self.data_ws_thread.join()
  568. if self.recording_thread:
  569. self.recording_thread.join()
  570. # Clean up audio resources
  571. if self.stream:
  572. self.stream.stop_stream()
  573. self.stream.close()
  574. if self.audio_interface:
  575. self.audio_interface.terminate()
  576. def __enter__(self):
  577. """
  578. Method to setup the context manager protocol.
  579. This enables the instance to be used in a `with` statement, ensuring
  580. proper resource management. When the `with` block is entered, this
  581. method is automatically called.
  582. Returns:
  583. self: The current instance of the class.
  584. """
  585. return self
  586. def __exit__(self, exc_type, exc_value, traceback):
  587. """
  588. Method to define behavior when the context manager protocol exits.
  589. This is called when exiting the `with` block and ensures that any
  590. necessary cleanup or resource release processes are executed, such as
  591. shutting down the system properly.
  592. Args:
  593. exc_type (Exception or None): The type of the exception that
  594. caused the context to be exited, if any.
  595. exc_value (Exception or None): The exception instance that caused
  596. the context to be exited, if any.
  597. traceback (Traceback or None): The traceback corresponding to the
  598. exception, if any.
  599. """
  600. self.shutdown()