stt_cli_client.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. """
  2. This is a command-line client for the Speech-to-Text (STT) server.
  3. It records audio from the default input device and sends it to the server for speech recognition.
  4. It can also process commands to set parameters, get parameter values, or call methods on the server.
  5. Usage:
  6. stt [--control-url CONTROL_URL] [--data-url DATA_URL] [--debug] [--norealtime] [--set-param PARAM VALUE] [--call-method METHOD [ARGS ...]] [--get-param PARAM]
  7. Options:
  8. - `-c, --control, --control_url`: STT Control WebSocket URL; default `DEFAULT_CONTROL_URL`.
  9. - `-d, --data, --data_url`: STT Data WebSocket URL; default `DEFAULT_DATA_URL`.
  10. - `-D, --debug`: Enable debug mode.
  11. - `-n, --norealtime`: Disable real-time output.
  12. - `-W, --write`: Save recorded audio to a WAV file.
  13. - `-s, --set`: Set a recorder parameter with format `PARAM VALUE`; can be used multiple times.
  14. - `-m, --method`: Call a recorder method with optional arguments; can be used multiple times.
  15. - `-g, --get`: Get a recorder parameter's value; can be used multiple times.
  16. - `-l, --loop`: Continuously transcribe speech without exiting.
  17. """
  18. from urllib.parse import urlparse
  19. from queue import Queue
  20. import subprocess
  21. import threading
  22. import websocket
  23. import argparse
  24. import pyaudio
  25. import struct
  26. import socket
  27. import shutil
  28. import queue
  29. import json
  30. import time
  31. import wave
  32. import sys
  33. import os
  34. os.environ['ALSA_LOG_LEVEL'] = 'none'
  35. # Constants
  36. CHUNK = 1024
  37. FORMAT = pyaudio.paInt16
  38. CHANNELS = 1
  39. RATE = 44100
  40. DEFAULT_CONTROL_URL = "ws://127.0.0.1:8011"
  41. DEFAULT_DATA_URL = "ws://127.0.0.1:8012"
  42. # Initialize colorama
  43. from colorama import init, Fore, Style
  44. init()
  45. # Stop websocket from spamming the log
  46. websocket.enableTrace(False)
  47. class STTWebSocketClient:
  48. def __init__(self, control_url, data_url, debug=False, file_output=None, norealtime=False, writechunks=None, continuous=False):
  49. self.control_url = control_url
  50. self.data_url = data_url
  51. self.control_ws = None
  52. self.data_ws_app = None
  53. self.data_ws_connected = None
  54. self.is_running = True
  55. self.debug = debug
  56. self.file_output = file_output
  57. self.last_text = ""
  58. self.console_width = shutil.get_terminal_size().columns
  59. self.recording_indicator = "🔴"
  60. self.norealtime = norealtime
  61. self.connection_established = threading.Event()
  62. self.message_queue = Queue()
  63. self.commands = Queue()
  64. self.stop_event = threading.Event()
  65. self.chunks_sent = 0
  66. self.last_chunk_time = time.time()
  67. self.writechunks = writechunks
  68. self.continuous = continuous
  69. self.debug_print("Initializing STT WebSocket Client")
  70. self.debug_print(f"Control URL: {control_url}")
  71. self.debug_print(f"Data URL: {data_url}")
  72. self.debug_print(f"File Output: {file_output}")
  73. self.debug_print(f"No Realtime: {norealtime}")
  74. self.debug_print(f"Write Chunks: {writechunks}")
  75. self.debug_print(f"Continuous Mode: {continuous}")
  76. # Audio attributes
  77. self.audio_interface = None
  78. self.stream = None
  79. self.device_sample_rate = None
  80. self.input_device_index = None
  81. # Threads
  82. self.control_ws_thread = None
  83. self.data_ws_thread = None
  84. self.recording_thread = None
  85. def debug_print(self, message):
  86. if self.debug:
  87. timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
  88. thread_name = threading.current_thread().name
  89. print(f"{Fore.CYAN}[DEBUG][{timestamp}][{thread_name}] {message}{Style.RESET_ALL}", file=sys.stderr)
  90. def connect(self):
  91. if not self.ensure_server_running():
  92. self.debug_print("Cannot start STT server. Exiting.")
  93. return False
  94. try:
  95. self.debug_print("Attempting to establish WebSocket connections...")
  96. # Connect to control WebSocket
  97. self.debug_print(f"Connecting to control WebSocket at {self.control_url}")
  98. self.control_ws = websocket.WebSocketApp(self.control_url,
  99. on_message=self.on_control_message,
  100. on_error=self.on_error,
  101. on_close=self.on_close,
  102. on_open=self.on_control_open)
  103. self.control_ws_thread = threading.Thread(target=self.control_ws.run_forever)
  104. self.control_ws_thread.daemon = False
  105. self.debug_print("Starting control WebSocket thread")
  106. self.control_ws_thread.start()
  107. # Connect to data WebSocket
  108. self.debug_print(f"Connecting to data WebSocket at {self.data_url}")
  109. self.data_ws_app = websocket.WebSocketApp(self.data_url,
  110. on_message=self.on_data_message,
  111. on_error=self.on_error,
  112. on_close=self.on_close,
  113. on_open=self.on_data_open)
  114. self.data_ws_thread = threading.Thread(target=self.data_ws_app.run_forever)
  115. self.data_ws_thread.daemon = False
  116. self.debug_print("Starting data WebSocket thread")
  117. self.data_ws_thread.start()
  118. self.debug_print("Waiting for connections to be established...")
  119. if not self.connection_established.wait(timeout=10):
  120. self.debug_print("Timeout while connecting to the server.")
  121. return False
  122. self.debug_print("WebSocket connections established successfully.")
  123. return True
  124. except Exception as e:
  125. self.debug_print(f"Error while connecting to the server: {str(e)}")
  126. return False
  127. def on_control_open(self, ws):
  128. self.debug_print("Control WebSocket connection opened successfully")
  129. self.connection_established.set()
  130. self.start_command_processor()
  131. def on_data_open(self, ws):
  132. self.debug_print("Data WebSocket connection opened successfully")
  133. self.data_ws_connected = ws
  134. self.start_recording()
  135. def on_error(self, ws, error):
  136. self.debug_print(f"WebSocket error occurred: {str(error)}")
  137. self.debug_print(f"Error type: {type(error)}")
  138. def on_close(self, ws, close_status_code, close_msg):
  139. if ws == self.data_ws_connected:
  140. self.debug_print(f"Data connection closed (code {close_status_code}, msg: {close_msg})")
  141. elif ws == self.control_ws:
  142. self.debug_print(f"Control connection closed (code {close_status_code}, msg: {close_msg})")
  143. else:
  144. self.debug_print(f"Unknown connection closed (code {close_status_code}, msg: {close_msg})")
  145. self.is_running = False
  146. self.stop_event.set()
  147. def is_server_running(self):
  148. parsed_url = urlparse(self.control_url)
  149. host = parsed_url.hostname
  150. port = parsed_url.port or 80
  151. self.debug_print(f"Checking if server is running at {host}:{port}")
  152. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  153. result = s.connect_ex((host, port)) == 0
  154. self.debug_print(f"Server status check result: {'running' if result else 'not running'}")
  155. return result
  156. def ask_to_start_server(self):
  157. response = input("Would you like to start the STT server now? (y/n): ").strip().lower()
  158. return response == 'y' or response == 'yes'
  159. def start_server(self):
  160. if os.name == 'nt': # Windows
  161. subprocess.Popen('start /min cmd /c stt-server', shell=True)
  162. else: # Unix-like systems
  163. terminal_emulators = [
  164. 'gnome-terminal',
  165. 'x-terminal-emulator',
  166. 'konsole',
  167. 'xfce4-terminal',
  168. 'lxterminal',
  169. 'xterm',
  170. 'mate-terminal',
  171. 'terminator',
  172. 'tilix',
  173. 'alacritty',
  174. 'urxvt',
  175. 'eterm',
  176. 'rxvt',
  177. 'kitty',
  178. 'hyper'
  179. ]
  180. terminal = None
  181. for term in terminal_emulators:
  182. if shutil.which(term):
  183. terminal = term
  184. break
  185. if terminal:
  186. terminal_exec_options = {
  187. 'x-terminal-emulator': ['--'],
  188. 'gnome-terminal': ['--'],
  189. 'mate-terminal': ['--'],
  190. 'terminator': ['--'],
  191. 'tilix': ['--'],
  192. 'konsole': ['-e'],
  193. 'xfce4-terminal': ['-e'],
  194. 'lxterminal': ['-e'],
  195. 'alacritty': ['-e'],
  196. 'xterm': ['-e'],
  197. 'rxvt': ['-e'],
  198. 'urxvt': ['-e'],
  199. 'eterm': ['-e'],
  200. 'kitty': [],
  201. 'hyper': ['--command']
  202. }
  203. exec_option = terminal_exec_options.get(terminal, None)
  204. if exec_option is not None:
  205. subprocess.Popen([terminal] + exec_option + ['stt-server'], start_new_session=True)
  206. print(f"STT server started in a new terminal window using {terminal}.", file=sys.stderr)
  207. else:
  208. print(f"Unsupported terminal emulator '{terminal}'. Please start the STT server manually.", file=sys.stderr)
  209. else:
  210. print("No supported terminal emulator found. Please start the STT server manually.", file=sys.stderr)
  211. def ensure_server_running(self):
  212. if not self.is_server_running():
  213. print("STT server is not running.", file=sys.stderr)
  214. if self.ask_to_start_server():
  215. self.start_server()
  216. print("Waiting for STT server to start...", file=sys.stderr)
  217. for _ in range(20): # Wait up to 20 seconds
  218. if self.is_server_running():
  219. print("STT server started successfully.", file=sys.stderr)
  220. time.sleep(2) # Give the server a moment to fully initialize
  221. return True
  222. time.sleep(1)
  223. print("Failed to start STT server.", file=sys.stderr)
  224. return False
  225. else:
  226. print("STT server is required. Please start it manually.", file=sys.stderr)
  227. return False
  228. return True
  229. def on_control_message(self, ws, message):
  230. try:
  231. self.debug_print(f"Received control message: {message}")
  232. data = json.loads(message)
  233. if 'status' in data:
  234. self.debug_print(f"Message status: {data['status']}")
  235. if data['status'] == 'success':
  236. if 'parameter' in data and 'value' in data:
  237. self.debug_print(f"Parameter update: {data['parameter']} = {data['value']}")
  238. print(f"Parameter {data['parameter']} = {data['value']}")
  239. elif data['status'] == 'error':
  240. self.debug_print(f"Server error received: {data.get('message', '')}")
  241. print(f"Server Error: {data.get('message', '')}")
  242. else:
  243. self.debug_print(f"Unknown control message format: {data}")
  244. except json.JSONDecodeError:
  245. self.debug_print(f"Failed to decode JSON control message: {message}")
  246. except Exception as e:
  247. self.debug_print(f"Error processing control message: {str(e)}")
  248. def on_data_message(self, ws, message):
  249. try:
  250. self.debug_print(f"Received data message: {message}")
  251. data = json.loads(message)
  252. message_type = data.get('type')
  253. self.debug_print(f"Message type: {message_type}")
  254. if message_type == 'realtime':
  255. if data['text'] != self.last_text:
  256. self.debug_print(f"New realtime text received: {data['text']}")
  257. self.last_text = data['text']
  258. if not self.norealtime:
  259. self.update_progress_bar(self.last_text)
  260. elif message_type == 'fullSentence':
  261. self.debug_print(f"Full sentence received: {data['text']}")
  262. if self.file_output:
  263. self.debug_print("Writing to file output")
  264. sys.stderr.write('\r\033[K')
  265. sys.stderr.write(data['text'])
  266. sys.stderr.write('\n')
  267. sys.stderr.flush()
  268. print(data['text'], file=self.file_output)
  269. self.file_output.flush()
  270. else:
  271. self.finish_progress_bar()
  272. print(f"{data['text']}")
  273. if not self.continuous:
  274. self.is_running = False
  275. self.stop_event.set()
  276. elif message_type in {
  277. 'vad_detect_start',
  278. 'vad_detect_stop',
  279. 'recording_start',
  280. 'recording_stop',
  281. 'wakeword_detected',
  282. 'wakeword_detection_start',
  283. 'wakeword_detection_end',
  284. 'transcription_start'}:
  285. pass # Known message types, no action needed
  286. else:
  287. self.debug_print(f"Other message type received: {message_type}")
  288. except json.JSONDecodeError:
  289. self.debug_print(f"Failed to decode JSON data message: {message}")
  290. except Exception as e:
  291. self.debug_print(f"Error processing data message: {str(e)}")
  292. def show_initial_indicator(self):
  293. if self.norealtime:
  294. return
  295. initial_text = f"{self.recording_indicator}\b\b"
  296. sys.stderr.write(initial_text)
  297. sys.stderr.flush()
  298. def update_progress_bar(self, text):
  299. try:
  300. available_width = self.console_width - 5 # Adjust for progress bar decorations
  301. sys.stderr.write('\r\033[K') # Clear the current line
  302. words = text.split()
  303. last_chars = ""
  304. for word in reversed(words):
  305. if len(last_chars) + len(word) + 1 > available_width:
  306. break
  307. last_chars = word + " " + last_chars
  308. last_chars = last_chars.strip()
  309. colored_text = f"{Fore.YELLOW}{last_chars}{Style.RESET_ALL}{self.recording_indicator}\b\b"
  310. sys.stderr.write(colored_text)
  311. sys.stderr.flush()
  312. except Exception as e:
  313. self.debug_print(f"Error updating progress bar: {e}")
  314. def finish_progress_bar(self):
  315. try:
  316. sys.stderr.write('\r\033[K')
  317. sys.stderr.flush()
  318. except Exception as e:
  319. self.debug_print(f"Error finishing progress bar: {e}")
  320. def stop(self):
  321. self.finish_progress_bar()
  322. self.is_running = False
  323. self.stop_event.set()
  324. self.debug_print("Stopping client and cleaning up resources.")
  325. if self.control_ws:
  326. self.control_ws.close()
  327. if self.data_ws_connected:
  328. self.data_ws_connected.close()
  329. # Join threads to ensure they finish before exiting
  330. current_thread = threading.current_thread()
  331. if self.control_ws_thread and self.control_ws_thread != current_thread:
  332. self.control_ws_thread.join()
  333. if self.data_ws_thread and self.data_ws_thread != current_thread:
  334. self.data_ws_thread.join()
  335. if self.recording_thread and self.recording_thread != current_thread:
  336. self.recording_thread.join()
  337. # Clean up audio resources
  338. if self.stream:
  339. self.stream.stop_stream()
  340. self.stream.close()
  341. if self.audio_interface:
  342. self.audio_interface.terminate()
  343. def start_recording(self):
  344. self.recording_thread = threading.Thread(target=self.record_and_send_audio)
  345. self.recording_thread.daemon = False # Set to False to ensure proper shutdown
  346. self.recording_thread.start()
  347. def record_and_send_audio(self):
  348. try:
  349. if not self.setup_audio():
  350. self.debug_print("Failed to set up audio recording")
  351. raise Exception("Failed to set up audio recording.")
  352. # Initialize WAV file writer if writechunks is provided
  353. if self.writechunks:
  354. self.wav_file = wave.open(self.writechunks, 'wb')
  355. self.wav_file.setnchannels(CHANNELS)
  356. self.wav_file.setsampwidth(pyaudio.get_sample_size(FORMAT))
  357. self.wav_file.setframerate(self.device_sample_rate) # Use self.device_sample_rate
  358. self.debug_print("Starting audio recording and transmission")
  359. self.show_initial_indicator()
  360. while self.is_running and not self.stop_event.is_set():
  361. try:
  362. audio_data = self.stream.read(CHUNK)
  363. self.chunks_sent += 1
  364. current_time = time.time()
  365. elapsed = current_time - self.last_chunk_time
  366. # Write to WAV file if enabled
  367. if self.writechunks:
  368. self.wav_file.writeframes(audio_data)
  369. if self.chunks_sent % 100 == 0: # Log every 100 chunks
  370. self.debug_print(f"Sent {self.chunks_sent} chunks. Last chunk took {elapsed:.3f}s")
  371. metadata = {"sampleRate": self.device_sample_rate}
  372. metadata_json = json.dumps(metadata)
  373. metadata_length = len(metadata_json)
  374. message = struct.pack('<I', metadata_length) + metadata_json.encode('utf-8') + audio_data
  375. if self.is_running and not self.stop_event.is_set():
  376. self.debug_print(f"Sending audio chunk {self.chunks_sent}: {len(audio_data)} bytes, metadata: {metadata_json}")
  377. self.data_ws_connected.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
  378. self.last_chunk_time = current_time
  379. except Exception as e:
  380. self.debug_print(f"Error sending audio data: {str(e)}")
  381. break
  382. except Exception as e:
  383. self.debug_print(f"Error in record_and_send_audio: {str(e)}")
  384. finally:
  385. self.cleanup_audio()
  386. def setup_audio(self):
  387. try:
  388. self.debug_print("Initializing PyAudio interface")
  389. self.audio_interface = pyaudio.PyAudio()
  390. self.input_device_index = None
  391. try:
  392. default_device = self.audio_interface.get_default_input_device_info()
  393. self.input_device_index = default_device['index']
  394. self.debug_print(f"Default input device found: {default_device}")
  395. except OSError as e:
  396. self.debug_print(f"No default input device found: {str(e)}")
  397. return False
  398. self.device_sample_rate = 16000
  399. self.debug_print(f"Attempting to open audio stream with sample rate {self.device_sample_rate} Hz")
  400. try:
  401. self.stream = self.audio_interface.open(
  402. format=FORMAT,
  403. channels=CHANNELS,
  404. rate=self.device_sample_rate,
  405. input=True,
  406. frames_per_buffer=CHUNK,
  407. input_device_index=self.input_device_index,
  408. )
  409. self.debug_print(f"Audio stream initialized successfully")
  410. self.debug_print(f"Audio parameters: rate={self.device_sample_rate}, channels={CHANNELS}, format={FORMAT}, chunk={CHUNK}")
  411. return True
  412. except Exception as e:
  413. self.debug_print(f"Failed to initialize audio stream: {str(e)}")
  414. return False
  415. except Exception as e:
  416. self.debug_print(f"Error in setup_audio: {str(e)}")
  417. if self.audio_interface:
  418. self.audio_interface.terminate()
  419. return False
  420. def cleanup_audio(self):
  421. self.debug_print("Cleaning up audio resources")
  422. try:
  423. if self.stream:
  424. self.debug_print("Stopping and closing audio stream")
  425. self.stream.stop_stream()
  426. self.stream.close()
  427. self.stream = None
  428. if self.audio_interface:
  429. self.debug_print("Terminating PyAudio interface")
  430. self.audio_interface.terminate()
  431. self.audio_interface = None
  432. if self.writechunks and self.wav_file:
  433. self.debug_print("Closing WAV file")
  434. self.wav_file.close()
  435. except Exception as e:
  436. self.debug_print(f"Error during audio cleanup: {str(e)}")
  437. def set_parameter(self, parameter, value):
  438. command = {
  439. "command": "set_parameter",
  440. "parameter": parameter,
  441. "value": value
  442. }
  443. self.control_ws.send(json.dumps(command))
  444. def get_parameter(self, parameter):
  445. command = {
  446. "command": "get_parameter",
  447. "parameter": parameter
  448. }
  449. self.control_ws.send(json.dumps(command))
  450. def call_method(self, method, args=None, kwargs=None):
  451. command = {
  452. "command": "call_method",
  453. "method": method,
  454. "args": args or [],
  455. "kwargs": kwargs or {}
  456. }
  457. self.control_ws.send(json.dumps(command))
  458. def start_command_processor(self):
  459. self.command_thread = threading.Thread(target=self.command_processor)
  460. self.command_thread.daemon = False # Ensure it is not a daemon thread
  461. self.command_thread.start()
  462. def command_processor(self):
  463. self.debug_print("Starting command processor thread")
  464. while not self.stop_event.is_set():
  465. try:
  466. command = self.commands.get(timeout=0.1)
  467. self.debug_print(f"Processing command: {command}")
  468. if command['type'] == 'set_parameter':
  469. self.debug_print(f"Setting parameter: {command['parameter']} = {command['value']}")
  470. self.set_parameter(command['parameter'], command['value'])
  471. elif command['type'] == 'get_parameter':
  472. self.debug_print(f"Getting parameter: {command['parameter']}")
  473. self.get_parameter(command['parameter'])
  474. elif command['type'] == 'call_method':
  475. self.debug_print(f"Calling method: {command['method']} with args: {command.get('args')} and kwargs: {command.get('kwargs')}")
  476. self.call_method(command['method'], command.get('args'), command.get('kwargs'))
  477. except queue.Empty:
  478. continue
  479. except Exception as e:
  480. self.debug_print(f"Error in command processor: {str(e)}")
  481. self.debug_print("Command processor thread stopping")
  482. def add_command(self, command):
  483. self.commands.put(command)
  484. def main():
  485. parser = argparse.ArgumentParser(description="STT Client")
  486. parser.add_argument("-c", "--control", "--control_url", default=DEFAULT_CONTROL_URL,
  487. help="STT Control WebSocket URL")
  488. parser.add_argument("-d", "--data", "--data_url", default=DEFAULT_DATA_URL,
  489. help="STT Data WebSocket URL")
  490. parser.add_argument("-D", "--debug", action="store_true",
  491. help="Enable debug mode")
  492. parser.add_argument("-n", "--norealtime", action="store_true",
  493. help="Disable real-time output")
  494. parser.add_argument("-W", "--write", metavar="FILE",
  495. help="Save recorded audio to a WAV file")
  496. parser.add_argument("-s", "--set", nargs=2, metavar=('PARAM', 'VALUE'), action='append',
  497. help="Set a recorder parameter (can be used multiple times)")
  498. parser.add_argument("-m", "--method", nargs='+', metavar='METHOD', action='append',
  499. help="Call a recorder method with optional arguments")
  500. parser.add_argument("-g", "--get", nargs=1, metavar='PARAM', action='append',
  501. help="Get a recorder parameter's value (can be used multiple times)")
  502. parser.add_argument("-l", "--loop", action="store_true",
  503. help="Continuously transcribe speech without exiting")
  504. args = parser.parse_args()
  505. # Check if output is being redirected
  506. if not os.isatty(sys.stdout.fileno()):
  507. file_output = sys.stdout
  508. else:
  509. file_output = None
  510. client = STTWebSocketClient(
  511. args.control,
  512. args.data,
  513. args.debug,
  514. file_output,
  515. args.norealtime, # Adjusted logic for real-time output
  516. args.write,
  517. continuous=args.loop
  518. )
  519. def signal_handler(sig, frame):
  520. client.stop()
  521. sys.exit(0)
  522. import signal
  523. signal.signal(signal.SIGINT, signal_handler)
  524. try:
  525. if client.connect():
  526. # Process command-line parameters
  527. if args.set:
  528. for param, value in args.set:
  529. try:
  530. if '.' in value:
  531. value = float(value)
  532. else:
  533. value = int(value)
  534. except ValueError:
  535. pass # Keep as string if not a number
  536. client.add_command({
  537. 'type': 'set_parameter',
  538. 'parameter': param,
  539. 'value': value
  540. })
  541. if args.get:
  542. for param_list in args.get:
  543. param = param_list[0]
  544. client.add_command({
  545. 'type': 'get_parameter',
  546. 'parameter': param
  547. })
  548. if args.method:
  549. for method_call in args.method:
  550. method = method_call[0]
  551. args_list = method_call[1:] if len(method_call) > 1 else []
  552. client.add_command({
  553. 'type': 'call_method',
  554. 'method': method,
  555. 'args': args_list
  556. })
  557. # If command-line parameters were used (like --get-param), wait for them to be processed
  558. if args.set or args.get or args.method:
  559. while not client.commands.empty():
  560. time.sleep(0.1)
  561. # Start recording directly if no command-line params were provided
  562. while client.is_running:
  563. time.sleep(0.1)
  564. else:
  565. print("Failed to connect to the server.", file=sys.stderr)
  566. except Exception as e:
  567. print(f"An error occurred: {e}")
  568. finally:
  569. client.stop()
  570. if __name__ == "__main__":
  571. main()