[Frontend] Add configurable output sample rate to Speech API (#6553)
Signed-off-by: 郑倩倩 <2458983186@qq.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
a65e89cbd4
commit
49c6fc1540
12 files changed
+453
-14
No files matched your search
@@ -2953,8 +2953,10 @@ class TestStreamingResponse:
|
||||
usage_acc=None,
|
||||
tts_params=None,
|
||||
collect=None,
|
||||
target_sample_rate=None,
|
||||
):
|
||||
captured["tts_params"] = tts_params
|
||||
captured["target_sample_rate"] = target_sample_rate
|
||||
yield b"\x00\x00"
|
||||
|
||||
monkeypatch.setattr(speech_server, "_prepare_speech_generation", prepare)
|
||||
@@ -2967,6 +2969,7 @@ class TestStreamingResponse:
|
||||
|
||||
assert response.status_code == 200
|
||||
assert captured["tts_params"] is finalized_tts_params
|
||||
assert captured["target_sample_rate"] is None
|
||||
|
||||
def test_sse_streaming(self, streaming_app):
|
||||
"""stream_format=sse without stream=True returns audio deltas as SSE."""
|
||||
@@ -3292,6 +3295,28 @@ class TestMergeBatchItem:
|
||||
|
||||
assert merged.non_streaming_mode is False
|
||||
|
||||
def test_sample_rate_batch_default_used(self):
|
||||
"""Batch-level sample_rate should be used when the item omits it."""
|
||||
batch = BatchSpeechRequest(
|
||||
items=[SpeechBatchItem(input="hi")],
|
||||
sample_rate=8000,
|
||||
)
|
||||
|
||||
merged = OmniOpenAIServingSpeech._merge_batch_item(batch, batch.items[0])
|
||||
|
||||
assert merged.sample_rate == 8000
|
||||
|
||||
def test_sample_rate_item_override_wins(self):
|
||||
"""Per-item sample_rate should override the batch-level default."""
|
||||
batch = BatchSpeechRequest(
|
||||
items=[SpeechBatchItem(input="hi", sample_rate=24000)],
|
||||
sample_rate=8000,
|
||||
)
|
||||
|
||||
merged = OmniOpenAIServingSpeech._merge_batch_item(batch, batch.items[0])
|
||||
|
||||
assert merged.sample_rate == 24000
|
||||
|
||||
|
||||
def test_streaming_speech_session_config_accepts_non_streaming_mode():
|
||||
config = StreamingSpeechSessionConfig(non_streaming_mode=True)
|
||||
@@ -3299,6 +3324,33 @@ def test_streaming_speech_session_config_accepts_non_streaming_mode():
|
||||
assert config.non_streaming_mode is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sample_rate", [8000, 24000])
|
||||
def test_speech_requests_accept_supported_sample_rates(sample_rate):
|
||||
assert OpenAICreateSpeechRequest(input="hello", sample_rate=sample_rate).sample_rate == sample_rate
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sample_rate", [0, -8000])
|
||||
def test_speech_request_rejects_non_positive_sample_rate(sample_rate):
|
||||
with pytest.raises(ValidationError):
|
||||
OpenAICreateSpeechRequest(input="hello", sample_rate=sample_rate)
|
||||
|
||||
|
||||
def test_sample_rate_uses_adapter_capabilities():
|
||||
server = object.__new__(OmniOpenAIServingSpeech)
|
||||
request = OpenAICreateSpeechRequest(input="hello", sample_rate=8000)
|
||||
|
||||
server._tts_model_type = "qwen3_tts"
|
||||
assert server._validate_speech_sample_rate(request) is None
|
||||
|
||||
request.sample_rate = 16000
|
||||
assert server._validate_speech_sample_rate(request) == (
|
||||
"sample_rate=16000 is not supported by the current TTS model; supported rates: 8000, 24000"
|
||||
)
|
||||
|
||||
server._tts_model_type = "fish_speech"
|
||||
assert server._validate_speech_sample_rate(request) == ("sample_rate is not supported by the current TTS model")
|
||||
|
||||
|
||||
def test_streaming_speech_session_config_scopes_native_speed_to_http():
|
||||
with pytest.raises(
|
||||
ValidationError,
|
||||
@@ -5027,6 +5079,78 @@ class TestTTSAsyncOffloading:
|
||||
assert "partial_audio" not in payload["error"]
|
||||
assert "action" not in payload["error"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_audio_chunks_resamples_pcm_to_8khz(self, qwen3_tts_server):
|
||||
async def pcm_generator():
|
||||
output = create_mock_audio_output_for_test()
|
||||
output.multimodal_output.update(
|
||||
audio=torch.linspace(-0.5, 0.5, 2400, dtype=torch.float32),
|
||||
sr=24000,
|
||||
)
|
||||
yield output
|
||||
|
||||
chunks = [
|
||||
chunk
|
||||
async for chunk in qwen3_tts_server._generate_audio_chunks(
|
||||
pcm_generator(),
|
||||
"req-8khz",
|
||||
target_sample_rate=8000,
|
||||
)
|
||||
]
|
||||
|
||||
assert len(chunks) >= 2
|
||||
assert len(b"".join(chunks)) == 800 * 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_audio_chunks_wav_header_uses_target_sample_rate(self, qwen3_tts_server):
|
||||
async def pcm_generator():
|
||||
output = create_mock_audio_output_for_test()
|
||||
output.multimodal_output.update(
|
||||
audio=torch.zeros(2400, dtype=torch.float32),
|
||||
sr=24000,
|
||||
)
|
||||
yield output
|
||||
|
||||
chunks = [
|
||||
chunk
|
||||
async for chunk in qwen3_tts_server._generate_audio_chunks(
|
||||
pcm_generator(),
|
||||
"req-8khz-wav",
|
||||
response_format="wav",
|
||||
target_sample_rate=8000,
|
||||
)
|
||||
]
|
||||
|
||||
assert struct.unpack("<I", chunks[0][24:28])[0] == 8000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_resampler_retains_first_chunk_sample_rate_metadata(self, qwen3_tts_server):
|
||||
async def pcm_generator():
|
||||
first = create_mock_audio_output_for_test()
|
||||
first.multimodal_output.update(
|
||||
audio=torch.zeros(10, dtype=torch.float32),
|
||||
sr=24000,
|
||||
)
|
||||
yield first
|
||||
|
||||
second = create_mock_audio_output_for_test()
|
||||
second.multimodal_output.update(audio=torch.zeros(230, dtype=torch.float32))
|
||||
second.multimodal_output.pop("sr", None)
|
||||
yield second
|
||||
|
||||
chunks = [
|
||||
chunk
|
||||
async for chunk in qwen3_tts_server._generate_audio_chunks(
|
||||
pcm_generator(),
|
||||
"req-retained-sr",
|
||||
response_format="wav",
|
||||
target_sample_rate=8000,
|
||||
)
|
||||
]
|
||||
|
||||
assert chunks[0][:4] == b"RIFF"
|
||||
assert struct.unpack("<I", chunks[0][24:28])[0] == 8000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_audio_chunks_discards_ref_audio_artifact_warmup_on_close(self, qwen3_tts_server):
|
||||
async def pcm_generator():
|
||||
|
||||
Reference in new issue
Block a user