When the TTS WebSocket closes part way through a stream without sending complete and without an error, synthesize returns the audio it got so far as though that were the whole answer. Nothing raises and nothing on the object says the stream was cut short, so a caller writing the chunks to a file gets truncated audio and no way to know.
_on_close is where it happens:
def _on_close(self, ws, *args):
self.is_connected = False
if not self.is_complete:
self.audio_queue.put(None)
None is the same sentinel the complete branch uses, so the consume loop reads a dropped connection and a finished stream identically and breaks out of the loop either way.
Reproducing
A stand-in socket that delivers two chunks and then closes cleanly, which is what a load balancer timing out an idle upstream or a worker restart looks like from the client side:
import json
from unittest.mock import patch
from smallestai.waves.stream_tts import TTSConfig, WavesStreamingTTS
class FakeWS:
def __init__(self, url, header=None, on_open=None, on_message=None, on_error=None, on_close=None):
self.on_open, self.on_message, self.on_close = on_open, on_message, on_close
def run_forever(self):
self.on_open(self)
import time; time.sleep(0.4)
self.on_message(self, json.dumps({"status": "", "data": {"audio": "YQ=="}}))
self.on_message(self, json.dumps({"status": "", "data": {"audio": "Yg=="}}))
self.on_close(self) # no "complete", no error
def send(self, payload): pass
def close(self): pass
with patch("smallestai.waves.stream_tts.WebSocketApp", FakeWS):
tts = WavesStreamingTTS(TTSConfig(voice_id="magnus", api_key="k"))
chunks = list(tts.synthesize("a long sentence the server never finished"))
print("chunks received :", chunks)
print("is_complete :", tts.is_complete)
chunks received : [b'a', b'b']
is_complete : False
raised : nothing
is_complete is False, so the object knows the stream never finished. The caller just never gets told.
What I think it should do
Raising on a close that arrives before complete seems right to me, since a partial answer presented as a whole one is worse than an error a caller can retry. But that turns a silent truncation into an exception for anyone relying on the current behaviour, so it is your call rather than mine. Two milder options if you would rather not break that: leave is_complete as the documented way to check after the generator ends, or put the truncation on the error queue only when at least one chunk was received.
I left this out of #112 for that reason. That PR only closes sockets and changes no behaviour. Happy to send whichever of these you prefer.
Found while reading the module for #112, on main at fb2502c.
When the TTS WebSocket closes part way through a stream without sending
completeand without an error,synthesizereturns the audio it got so far as though that were the whole answer. Nothing raises and nothing on the object says the stream was cut short, so a caller writing the chunks to a file gets truncated audio and no way to know._on_closeis where it happens:Noneis the same sentinel thecompletebranch uses, so the consume loop reads a dropped connection and a finished stream identically and breaks out of the loop either way.Reproducing
A stand-in socket that delivers two chunks and then closes cleanly, which is what a load balancer timing out an idle upstream or a worker restart looks like from the client side:
is_completeisFalse, so the object knows the stream never finished. The caller just never gets told.What I think it should do
Raising on a close that arrives before
completeseems right to me, since a partial answer presented as a whole one is worse than an error a caller can retry. But that turns a silent truncation into an exception for anyone relying on the current behaviour, so it is your call rather than mine. Two milder options if you would rather not break that: leaveis_completeas the documented way to check after the generator ends, or put the truncation on the error queue only when at least one chunk was received.I left this out of #112 for that reason. That PR only closes sockets and changes no behaviour. Happy to send whichever of these you prefer.
Found while reading the module for #112, on
mainat fb2502c.