[Frontend] Opt-in WebSocket TTS split_granularity and session seed (#7046)
Signed-off-by: rk9595 <rakesh.kariya@somaiya.edu> Signed-off-by: Rakesh Kariya <rakesh.kariya@somaiya.edu> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
8 files changed
+610
-52
No files matched your search
@@ -197,7 +197,7 @@ class TestStreamingSpeechWebSocket:
|
||||
|
||||
error = ws.receive_json()
|
||||
assert error["type"] == "error"
|
||||
assert "while input is buffered" in error["message"]
|
||||
assert "while an utterance is in progress" in error["message"]
|
||||
|
||||
# The buffered text survives the rejected reconfiguration.
|
||||
ws.send_json({"type": "input.done"})
|
||||
@@ -742,6 +742,8 @@ class TestStreamingSpeechWebSocket:
|
||||
config.speaker_embedding = None
|
||||
config.stream_audio = True
|
||||
config.word_timestamps = False
|
||||
config.seed = None
|
||||
config.non_streaming_mode = None
|
||||
|
||||
with pytest.raises(WebSocketDisconnect):
|
||||
asyncio.run(
|
||||
@@ -758,6 +760,163 @@ class TestStreamingSpeechWebSocket:
|
||||
assert websocket.send_json.await_count == 2
|
||||
|
||||
|
||||
class TestWebSocketSentenceSplitting:
|
||||
def test_sentence_granularity_emits_one_request_per_sentence(self, mocker: MockerFixture):
|
||||
app, speech_service = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
ws.send_json(
|
||||
{
|
||||
"type": "session.config",
|
||||
"voice": "Vivian",
|
||||
"split_granularity": "sentence",
|
||||
}
|
||||
)
|
||||
ws.send_json({"type": "input.text", "text": "Hello world. How are you? "})
|
||||
ws.send_json({"type": "input.done"})
|
||||
|
||||
first = ws.receive_json()
|
||||
assert first["sentence_index"] == 0
|
||||
assert first["sentence_text"] == "Hello world."
|
||||
ws.receive_bytes()
|
||||
assert ws.receive_json()["type"] == "audio.done"
|
||||
|
||||
second = ws.receive_json()
|
||||
assert second["sentence_index"] == 1
|
||||
assert second["sentence_text"] == "How are you?"
|
||||
ws.receive_bytes()
|
||||
assert ws.receive_json()["type"] == "audio.done"
|
||||
|
||||
assert ws.receive_json() == {
|
||||
"type": "session.done",
|
||||
"utterance_index": 0,
|
||||
"total_sentences": 2,
|
||||
}
|
||||
|
||||
assert speech_service._generate_audio_bytes.await_count == 2
|
||||
assert [call.args[0].input for call in speech_service._generate_audio_bytes.await_args_list] == [
|
||||
"Hello world.",
|
||||
"How are you?",
|
||||
]
|
||||
|
||||
def test_sentence_granularity_emits_before_input_done(self, mocker: MockerFixture):
|
||||
app, speech_service = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
ws.send_json(
|
||||
{
|
||||
"type": "session.config",
|
||||
"voice": "Vivian",
|
||||
"split_granularity": "sentence",
|
||||
}
|
||||
)
|
||||
ws.send_json({"type": "input.text", "text": "First sentence. "})
|
||||
|
||||
start = ws.receive_json()
|
||||
assert start["sentence_text"] == "First sentence."
|
||||
ws.receive_bytes()
|
||||
assert ws.receive_json()["type"] == "audio.done"
|
||||
assert speech_service._generate_audio_bytes.await_count == 1
|
||||
|
||||
ws.send_json({"type": "input.done"})
|
||||
assert ws.receive_json() == {
|
||||
"type": "session.done",
|
||||
"utterance_index": 0,
|
||||
"total_sentences": 1,
|
||||
}
|
||||
|
||||
def test_indic_danda_splits_without_latin_period(self, mocker: MockerFixture):
|
||||
app, speech_service = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
ws.send_json(
|
||||
{
|
||||
"type": "session.config",
|
||||
"voice": "Vivian",
|
||||
"language": "Auto",
|
||||
"split_granularity": "sentence",
|
||||
}
|
||||
)
|
||||
ws.send_json({"type": "input.text", "text": "नमस्ते। कैसे हो?"})
|
||||
ws.send_json({"type": "input.done"})
|
||||
|
||||
first = ws.receive_json()
|
||||
assert first["sentence_text"] == "नमस्ते।"
|
||||
ws.receive_bytes()
|
||||
assert ws.receive_json()["type"] == "audio.done"
|
||||
|
||||
second = ws.receive_json()
|
||||
assert second["sentence_text"] == "कैसे हो?"
|
||||
ws.receive_bytes()
|
||||
assert ws.receive_json()["type"] == "audio.done"
|
||||
assert ws.receive_json()["total_sentences"] == 2
|
||||
|
||||
def test_session_config_rejected_after_a_split_unit_was_emitted(self, mocker: MockerFixture):
|
||||
"""The splitter buffer is empty here, but the utterance is still open."""
|
||||
app, speech_service = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
ws.send_json(
|
||||
{
|
||||
"type": "session.config",
|
||||
"voice": "Vivian",
|
||||
"split_granularity": "sentence",
|
||||
}
|
||||
)
|
||||
ws.send_json({"type": "input.text", "text": "First sentence. "})
|
||||
ws.receive_json()
|
||||
ws.receive_bytes()
|
||||
assert ws.receive_json()["type"] == "audio.done"
|
||||
|
||||
ws.send_json({"type": "session.config", "voice": "Serena"})
|
||||
error = ws.receive_json()
|
||||
assert error["type"] == "error"
|
||||
assert "while an utterance is in progress" in error["message"]
|
||||
|
||||
ws.send_json({"type": "input.text", "text": "Second sentence. "})
|
||||
assert ws.receive_json()["sentence_index"] == 1
|
||||
ws.receive_bytes()
|
||||
assert ws.receive_json()["type"] == "audio.done"
|
||||
|
||||
ws.send_json({"type": "input.done"})
|
||||
assert ws.receive_json() == {
|
||||
"type": "session.done",
|
||||
"utterance_index": 0,
|
||||
"total_sentences": 2,
|
||||
}
|
||||
|
||||
# Reconfiguration is allowed again once the utterance closed.
|
||||
ws.send_json({"type": "session.config", "voice": "Serena"})
|
||||
ws.send_json({"type": "input.text", "text": "Third."})
|
||||
ws.send_json({"type": "input.done"})
|
||||
ws.receive_json()
|
||||
ws.receive_bytes()
|
||||
ws.receive_json()
|
||||
ws.receive_json()
|
||||
|
||||
voices = [call.args[0].voice for call in speech_service._generate_audio_bytes.await_args_list]
|
||||
assert voices == ["Vivian", "Vivian", "Serena"]
|
||||
|
||||
def test_seed_is_forwarded_to_speech_request(self, mocker: MockerFixture):
|
||||
app, speech_service = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
ws.send_json({"type": "session.config", "voice": "Vivian", "seed": 42})
|
||||
ws.send_json({"type": "input.text", "text": "Hello."})
|
||||
ws.send_json({"type": "input.done"})
|
||||
ws.receive_json()
|
||||
ws.receive_bytes()
|
||||
ws.receive_json()
|
||||
ws.receive_json()
|
||||
|
||||
assert speech_service._generate_audio_bytes.await_args_list[0].args[0].seed == 42
|
||||
|
||||
|
||||
class TestGeneratePcmChunksContract:
|
||||
"""Guard: _generate_pcm_chunks must exist on OmniOpenAIServingSpeech.
|
||||
|
||||
|
||||
Reference in new issue
Block a user