stt_cli_client.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  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. 'transcription_start'}:
  282. pass # Known message types, no action needed
  283. else:
  284. self.debug_print(f"Other message type received: {message_type}")
  285. except json.JSONDecodeError:
  286. self.debug_print(f"Failed to decode JSON data message: {message}")
  287. except Exception as e:
  288. self.debug_print(f"Error processing data message: {str(e)}")
  289. def show_initial_indicator(self):
  290. if self.norealtime:
  291. return
  292. initial_text = f"{self.recording_indicator}\b\b"
  293. sys.stderr.write(initial_text)
  294. sys.stderr.flush()
  295. def update_progress_bar(self, text):
  296. try:
  297. available_width = self.console_width - 5 # Adjust for progress bar decorations
  298. sys.stderr.write('\r\033[K') # Clear the current line
  299. words = text.split()
  300. last_chars = ""
  301. for word in reversed(words):
  302. if len(last_chars) + len(word) + 1 > available_width:
  303. break
  304. last_chars = word + " " + last_chars
  305. last_chars = last_chars.strip()
  306. colored_text = f"{Fore.YELLOW}{last_chars}{Style.RESET_ALL}{self.recording_indicator}\b\b"
  307. sys.stderr.write(colored_text)
  308. sys.stderr.flush()
  309. except Exception as e:
  310. self.debug_print(f"Error updating progress bar: {e}")
  311. def finish_progress_bar(self):
  312. try:
  313. sys.stderr.write('\r\033[K')
  314. sys.stderr.flush()
  315. except Exception as e:
  316. self.debug_print(f"Error finishing progress bar: {e}")
  317. def stop(self):
  318. self.finish_progress_bar()
  319. self.is_running = False
  320. self.stop_event.set()
  321. self.debug_print("Stopping client and cleaning up resources.")
  322. if self.control_ws:
  323. self.control_ws.close()
  324. if self.data_ws_connected:
  325. self.data_ws_connected.close()
  326. # Join threads to ensure they finish before exiting
  327. current_thread = threading.current_thread()
  328. if self.control_ws_thread and self.control_ws_thread != current_thread:
  329. self.control_ws_thread.join()
  330. if self.data_ws_thread and self.data_ws_thread != current_thread:
  331. self.data_ws_thread.join()
  332. if self.recording_thread and self.recording_thread != current_thread:
  333. self.recording_thread.join()
  334. # Clean up audio resources
  335. if self.stream:
  336. self.stream.stop_stream()
  337. self.stream.close()
  338. if self.audio_interface:
  339. self.audio_interface.terminate()
  340. def start_recording(self):
  341. self.recording_thread = threading.Thread(target=self.record_and_send_audio)
  342. self.recording_thread.daemon = False # Set to False to ensure proper shutdown
  343. self.recording_thread.start()
  344. def record_and_send_audio(self):
  345. try:
  346. if not self.setup_audio():
  347. self.debug_print("Failed to set up audio recording")
  348. raise Exception("Failed to set up audio recording.")
  349. # Initialize WAV file writer if writechunks is provided
  350. if self.writechunks:
  351. self.wav_file = wave.open(self.writechunks, 'wb')
  352. self.wav_file.setnchannels(CHANNELS)
  353. self.wav_file.setsampwidth(pyaudio.get_sample_size(FORMAT))
  354. self.wav_file.setframerate(self.device_sample_rate) # Use self.device_sample_rate
  355. self.debug_print("Starting audio recording and transmission")
  356. self.show_initial_indicator()
  357. while self.is_running and not self.stop_event.is_set():
  358. try:
  359. audio_data = self.stream.read(CHUNK)
  360. self.chunks_sent += 1
  361. current_time = time.time()
  362. elapsed = current_time - self.last_chunk_time
  363. # Write to WAV file if enabled
  364. if self.writechunks:
  365. self.wav_file.writeframes(audio_data)
  366. if self.chunks_sent % 100 == 0: # Log every 100 chunks
  367. self.debug_print(f"Sent {self.chunks_sent} chunks. Last chunk took {elapsed:.3f}s")
  368. metadata = {"sampleRate": self.device_sample_rate}
  369. metadata_json = json.dumps(metadata)
  370. metadata_length = len(metadata_json)
  371. message = struct.pack('<I', metadata_length) + metadata_json.encode('utf-8') + audio_data
  372. if self.is_running and not self.stop_event.is_set():
  373. self.debug_print(f"Sending audio chunk {self.chunks_sent}: {len(audio_data)} bytes, metadata: {metadata_json}")
  374. self.data_ws_connected.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
  375. self.last_chunk_time = current_time
  376. except Exception as e:
  377. self.debug_print(f"Error sending audio data: {str(e)}")
  378. break
  379. except Exception as e:
  380. self.debug_print(f"Error in record_and_send_audio: {str(e)}")
  381. finally:
  382. self.cleanup_audio()
  383. def setup_audio(self):
  384. try:
  385. self.debug_print("Initializing PyAudio interface")
  386. self.audio_interface = pyaudio.PyAudio()
  387. self.input_device_index = None
  388. try:
  389. default_device = self.audio_interface.get_default_input_device_info()
  390. self.input_device_index = default_device['index']
  391. self.debug_print(f"Default input device found: {default_device}")
  392. except OSError as e:
  393. self.debug_print(f"No default input device found: {str(e)}")
  394. return False
  395. self.device_sample_rate = 16000
  396. self.debug_print(f"Attempting to open audio stream with sample rate {self.device_sample_rate} Hz")
  397. try:
  398. self.stream = self.audio_interface.open(
  399. format=FORMAT,
  400. channels=CHANNELS,
  401. rate=self.device_sample_rate,
  402. input=True,
  403. frames_per_buffer=CHUNK,
  404. input_device_index=self.input_device_index,
  405. )
  406. self.debug_print(f"Audio stream initialized successfully")
  407. self.debug_print(f"Audio parameters: rate={self.device_sample_rate}, channels={CHANNELS}, format={FORMAT}, chunk={CHUNK}")
  408. return True
  409. except Exception as e:
  410. self.debug_print(f"Failed to initialize audio stream: {str(e)}")
  411. return False
  412. except Exception as e:
  413. self.debug_print(f"Error in setup_audio: {str(e)}")
  414. if self.audio_interface:
  415. self.audio_interface.terminate()
  416. return False
  417. def cleanup_audio(self):
  418. self.debug_print("Cleaning up audio resources")
  419. try:
  420. if self.stream:
  421. self.debug_print("Stopping and closing audio stream")
  422. self.stream.stop_stream()
  423. self.stream.close()
  424. self.stream = None
  425. if self.audio_interface:
  426. self.debug_print("Terminating PyAudio interface")
  427. self.audio_interface.terminate()
  428. self.audio_interface = None
  429. if self.writechunks and self.wav_file:
  430. self.debug_print("Closing WAV file")
  431. self.wav_file.close()
  432. except Exception as e:
  433. self.debug_print(f"Error during audio cleanup: {str(e)}")
  434. def set_parameter(self, parameter, value):
  435. command = {
  436. "command": "set_parameter",
  437. "parameter": parameter,
  438. "value": value
  439. }
  440. self.control_ws.send(json.dumps(command))
  441. def get_parameter(self, parameter):
  442. command = {
  443. "command": "get_parameter",
  444. "parameter": parameter
  445. }
  446. self.control_ws.send(json.dumps(command))
  447. def call_method(self, method, args=None, kwargs=None):
  448. command = {
  449. "command": "call_method",
  450. "method": method,
  451. "args": args or [],
  452. "kwargs": kwargs or {}
  453. }
  454. self.control_ws.send(json.dumps(command))
  455. def start_command_processor(self):
  456. self.command_thread = threading.Thread(target=self.command_processor)
  457. self.command_thread.daemon = False # Ensure it is not a daemon thread
  458. self.command_thread.start()
  459. def command_processor(self):
  460. self.debug_print("Starting command processor thread")
  461. while not self.stop_event.is_set():
  462. try:
  463. command = self.commands.get(timeout=0.1)
  464. self.debug_print(f"Processing command: {command}")
  465. if command['type'] == 'set_parameter':
  466. self.debug_print(f"Setting parameter: {command['parameter']} = {command['value']}")
  467. self.set_parameter(command['parameter'], command['value'])
  468. elif command['type'] == 'get_parameter':
  469. self.debug_print(f"Getting parameter: {command['parameter']}")
  470. self.get_parameter(command['parameter'])
  471. elif command['type'] == 'call_method':
  472. self.debug_print(f"Calling method: {command['method']} with args: {command.get('args')} and kwargs: {command.get('kwargs')}")
  473. self.call_method(command['method'], command.get('args'), command.get('kwargs'))
  474. except queue.Empty:
  475. continue
  476. except Exception as e:
  477. self.debug_print(f"Error in command processor: {str(e)}")
  478. self.debug_print("Command processor thread stopping")
  479. def add_command(self, command):
  480. self.commands.put(command)
  481. def main():
  482. parser = argparse.ArgumentParser(description="STT Client")
  483. parser.add_argument("-c", "--control", "--control_url", default=DEFAULT_CONTROL_URL,
  484. help="STT Control WebSocket URL")
  485. parser.add_argument("-d", "--data", "--data_url", default=DEFAULT_DATA_URL,
  486. help="STT Data WebSocket URL")
  487. parser.add_argument("-D", "--debug", action="store_true",
  488. help="Enable debug mode")
  489. parser.add_argument("-n", "--norealtime", action="store_true",
  490. help="Disable real-time output")
  491. parser.add_argument("-W", "--write", metavar="FILE",
  492. help="Save recorded audio to a WAV file")
  493. parser.add_argument("-s", "--set", nargs=2, metavar=('PARAM', 'VALUE'), action='append',
  494. help="Set a recorder parameter (can be used multiple times)")
  495. parser.add_argument("-m", "--method", nargs='+', metavar='METHOD', action='append',
  496. help="Call a recorder method with optional arguments")
  497. parser.add_argument("-g", "--get", nargs=1, metavar='PARAM', action='append',
  498. help="Get a recorder parameter's value (can be used multiple times)")
  499. parser.add_argument("-l", "--loop", action="store_true",
  500. help="Continuously transcribe speech without exiting")
  501. args = parser.parse_args()
  502. # Check if output is being redirected
  503. if not os.isatty(sys.stdout.fileno()):
  504. file_output = sys.stdout
  505. else:
  506. file_output = None
  507. client = STTWebSocketClient(
  508. args.control,
  509. args.data,
  510. args.debug,
  511. file_output,
  512. args.norealtime, # Adjusted logic for real-time output
  513. args.write,
  514. continuous=args.loop
  515. )
  516. def signal_handler(sig, frame):
  517. client.stop()
  518. sys.exit(0)
  519. import signal
  520. signal.signal(signal.SIGINT, signal_handler)
  521. try:
  522. if client.connect():
  523. # Process command-line parameters
  524. if args.set:
  525. for param, value in args.set:
  526. try:
  527. if '.' in value:
  528. value = float(value)
  529. else:
  530. value = int(value)
  531. except ValueError:
  532. pass # Keep as string if not a number
  533. client.add_command({
  534. 'type': 'set_parameter',
  535. 'parameter': param,
  536. 'value': value
  537. })
  538. if args.get:
  539. for param_list in args.get:
  540. param = param_list[0]
  541. client.add_command({
  542. 'type': 'get_parameter',
  543. 'parameter': param
  544. })
  545. if args.method:
  546. for method_call in args.method:
  547. method = method_call[0]
  548. args_list = method_call[1:] if len(method_call) > 1 else []
  549. client.add_command({
  550. 'type': 'call_method',
  551. 'method': method,
  552. 'args': args_list
  553. })
  554. # If command-line parameters were used (like --get-param), wait for them to be processed
  555. if args.set or args.get or args.method:
  556. while not client.commands.empty():
  557. time.sleep(0.1)
  558. # Start recording directly if no command-line params were provided
  559. while client.is_running:
  560. time.sleep(0.1)
  561. else:
  562. print("Failed to connect to the server.", file=sys.stderr)
  563. except Exception as e:
  564. print(f"An error occurred: {e}")
  565. finally:
  566. client.stop()
  567. if __name__ == "__main__":
  568. main()