Skip to content

Queue TTS messages#1

Open
alexstrangeparts wants to merge 1 commit intodev-alex-different-sitefrom
dev-alex-queue-tts-messages
Open

Queue TTS messages#1
alexstrangeparts wants to merge 1 commit intodev-alex-different-sitefrom
dev-alex-queue-tts-messages

Conversation

@alexstrangeparts
Copy link
Collaborator

  • user sends tts message
  • server queues tts message, sets “tts in progress flag” high
  • robot plays message in thread
  • based on a formula calculate how much time should pass, then send TTS_FINISHED message back to server
  • server de-queues next message and rinse and repeat

Copy link
Member

@scottyallen scottyallen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few questions for you

controller.py Outdated
log.debug("on_handle_chat_message(data)")
if data['type'] == 'message':
word_count = len(data['message'].split())
time_till_next_tts = (word_count * 1/2.5) + time.time() # How long in seconds a message should roughly take to read out at a rate of 150words/m
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there not a way to actually know when the tts playback finishes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no unfortunately not that i could do

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just wait for the thread created at

thread.start_new_thread(chat_module.handle_chat, args)
to return?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but then wouldn't that block on the main thread? or is there a way to do a callback for when the thread returns?

Copy link
Collaborator Author

@alexstrangeparts alexstrangeparts Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed in ac806fd to use thread.is_alive()

controller.py Outdated
@@ -437,8 +441,10 @@ def gps_is_enabled():
while not terminate.locked(): # Controller loop starts here
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the main control loop? If so, why isnt anythign else happening in it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

everything else is based on the async websocket. The only thing that happens in this loop atm is the gps geofence. Nothing else needs to happen

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. That makes sense. Can you write a comment to that effect?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a queuing mechanism for TTS (text-to-speech) messages on the robot. The changes allow the robot to notify the server when TTS playback completes, enabling proper message queuing.

Changes:

  • Added declareFinishedTTS() function to send completion notification to server
  • Modified message handling to only process 'message' type events for TTS
  • Added tracking of TTS thread state and automatic completion notification in main loop

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
networking.py Added declareFinishedTTS() function to send TTS_FINISHED event to server
controller.py Modified chat message handling to track TTS thread and notify server on completion

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

networking.py Outdated
Comment on lines 167 to 172
def declareFinishedTTS():
log.info("Finishing TTS message")
webSocket.send(json.dumps(
{"e": "TTS_FINISHED",
"d": {}
}))
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing closing parenthesis for json.dumps() call. This will cause a syntax error.

Copilot uses AI. Check for mistakes.
controller.py Outdated
global tts_thread
if chat_module == None:
thread.start_new_thread(handle_chat_message, args)
tts_thread = thread.start_new_thread(handle_chat_message, args)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thread.start_new_thread() returns a thread identifier (integer), not a thread object with an is_alive() method. This will cause an AttributeError at line 442 when trying to call tts_thread.is_alive(). Consider using the threading module's Thread class instead, which provides the is_alive() method.

Copilot uses AI. Check for mistakes.
controller.py Outdated
watchdog.watch()

if robot_config.getboolean('gps', 'enable_gps'):
if not tts_thread.is_alive():
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will attempt to check thread status even when tts_thread is None (its initial value). This will cause an AttributeError. Add a null check: if tts_thread is not None and not tts_thread.is_alive():

Suggested change
if not tts_thread.is_alive():
if tts_thread is not None and not tts_thread.is_alive():

Copilot uses AI. Check for mistakes.
if not test_mode:
log.critical('RemoTV Controller Started')
while not terminate.locked(): # Controller loop starts here
while not terminate.locked(): # Main control loop. This handles anythign that involves timing, everything else is asynchronous
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'anythign' to 'anything'.

Suggested change
while not terminate.locked(): # Main control loop. This handles anythign that involves timing, everything else is asynchronous
while not terminate.locked(): # Main control loop. This handles anything that involves timing, everything else is asynchronous

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

controller.py Outdated
Comment on lines 321 to 323
tts_thread = thread.start_new_thread(handle_chat_message, args)
else:
thread.start_new_thread(chat_module.handle_chat, args)
tts_thread = thread.start_new_thread(chat_module.handle_chat, args)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value of thread.start_new_thread() is an integer thread ID, not a Thread object. This means calling .is_alive() on tts_thread at line 442 will raise an AttributeError. To fix this, you should use the threading.Thread class instead, similar to how it's done in watchdog.py and gps.py in this codebase.

Copilot uses AI. Check for mistakes.
controller.py Outdated
watchdog.watch()

if robot_config.getboolean('gps', 'enable_gps'):
if not tts_thread.is_alive():
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global variable tts_thread is initialized to None, but the main loop at line 442 attempts to call .is_alive() on it without checking if it's None first. This will cause an AttributeError when the program starts (before any TTS message is received). You need to add a None check before calling .is_alive().

Suggested change
if not tts_thread.is_alive():
if tts_thread is not None and not tts_thread.is_alive():

Copilot uses AI. Check for mistakes.
controller.py Outdated
Comment on lines 442 to 443
if not tts_thread.is_alive():
networking.declareFinishedTTS()
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic will continuously send TTS_FINISHED messages to the server every second whenever there is no active TTS thread (including at startup before any TTS message has been received). According to the PR description, TTS_FINISHED should only be sent after a TTS message has completed. You should add a flag to track whether a TTS message is actually in progress and only send TTS_FINISHED when transitioning from active to inactive.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


if mute:
exit()
return False
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent handling of early termination: When mute is True, the function now returns False (line 111), but when a URL is filtered, it still calls exit() on line 139. This is inconsistent and exit() would terminate the entire process, not just the thread. For consistency and correctness, line 139 should also return False instead of calling exit().

Copilot uses AI. Check for mistakes.
Comment on lines +271 to 272
if data['type'] == 'message':
on_handle_chat_message(data)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message type filtering logic has been changed from data['type'] != "robot" (exclude robot messages) to data['type'] == 'message' (only process message type). This is a significant behavioral change that could affect what messages get processed. The old logic would process any message type except "robot", while the new logic only processes messages with type exactly equal to "message". If there are other message types (e.g., "user", "system", etc.) that were previously processed, they will now be ignored. Verify this change is intentional and won't break existing functionality.

Suggested change
if data['type'] == 'message':
on_handle_chat_message(data)
msg_type = data.get('type')
if msg_type is None:
log.error("Malformed message: missing 'type' field in MESSAGE_RECEIVED data: %s", data)
elif msg_type != 'robot':
on_handle_chat_message(data)

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +173
def declareFinishedTTS():
log.info("Finishing TTS message")
webSocket.send(json.dumps(
{
"e": "TTS_FINISHED",
"d": {}
}))
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential AttributeError: The function attempts to call webSocket.send() without checking if webSocket is None. If this function is called before the websocket is initialized, it will raise an AttributeError. Consider adding a check like if webSocket is None: return or if webSocket: before attempting to send.

Copilot uses AI. Check for mistakes.
Comment on lines +320 to +329
global tts_thread
if chat_module is None:
target = handle_chat_message
else:
target = chat_module.handle_chat

t = threading.Thread(target=target, args=args)
t.daemon = True # OK in Py2/Py3
t.start()
tts_thread = t
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: The tts_thread global variable can be accessed by multiple threads without synchronization. When a new chat message arrives while a TTS thread is still running, line 329 will overwrite the tts_thread reference, losing track of the previous thread. This breaks the queueing mechanism described in the PR, as only the most recently created thread will be tracked. Consider using a queue data structure (like collections.deque) to properly queue TTS messages, or add a lock to prevent concurrent access to tts_thread.

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +329
t = threading.Thread(target=target, args=args)
t.daemon = True # OK in Py2/Py3
t.start()
tts_thread = t
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation doesn't actually queue TTS messages as described in the PR description. The current code only tracks a single thread reference (tts_thread), which gets overwritten each time a new message arrives. This means if multiple TTS messages arrive rapidly, only the last one will be tracked for completion notification. To properly implement queueing, you should: 1) Check if tts_thread is alive before starting a new thread, 2) Either queue the message for later processing or implement a proper queue mechanism with a worker thread that processes messages sequentially.

Copilot uses AI. Check for mistakes.
Comment on lines +448 to +450
if tts_thread and not tts_thread.is_alive():
networking.declareFinishedTTS()
tts_thread = None
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition in the main loop: The check for tts_thread.is_alive() on line 448 can fail if tts_thread is set to None by line 450 between the time it's checked and when is_alive() is called. Additionally, another thread could overwrite tts_thread on line 329 while this check is executing. This should be protected with proper synchronization, such as storing the thread reference in a local variable or using a lock.

Suggested change
if tts_thread and not tts_thread.is_alive():
networking.declareFinishedTTS()
tts_thread = None
current_tts_thread = tts_thread
if current_tts_thread and not current_tts_thread.is_alive():
networking.declareFinishedTTS()
if tts_thread is current_tts_thread:
tts_thread = None

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants