Skip to content

Commit 04e06fa

Browse files
author
zhansheng.lzs
committed
fix(tts): attach response to HttpSpeechSynthesisResult so status_code is real
Non-streaming results were built without the underlying API response, making the proxied status_code always 0 and causing the CLI speech-synthesis create command to report successful synthesis as a failure. Failed responses now surface with real request_id/code/message instead of raising. Adds regression tests.
1 parent d7db049 commit 04e06fa

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

‎dashscope/audio/http_tts/http_speech_synthesizer.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,20 @@ def _handle_non_streaming_response(
266266
response,
267267
) -> HttpSpeechSynthesisResult:
268268
"""Handle non-streaming response."""
269+
if (
270+
isinstance(response, DashScopeAPIResponse)
271+
and response.status_code != HTTPStatus.OK
272+
):
273+
return HttpSpeechSynthesisResult(response=response)
274+
269275
output = cls._extract_output(response)
270276
audio_info = output.get("audio", {})
271277

272278
return HttpSpeechSynthesisResult(
273279
audio_url=audio_info.get("url"),
274280
audio_id=audio_info.get("id"),
275281
expires_at=audio_info.get("expires_at"),
282+
response=response,
276283
)
277284

278285
@classmethod
@@ -302,6 +309,7 @@ def _handle_streaming_response(
302309
yield HttpSpeechSynthesisResult(
303310
audio_data=audio_bytes,
304311
sentences=sentences.copy(),
312+
response=part,
305313
)
306314

307315
elif output.get("finish_reason") == "stop":
@@ -318,6 +326,7 @@ def _handle_streaming_response(
318326
"expires_at",
319327
),
320328
sentences=sentences.copy(),
329+
response=part,
321330
)
322331

323332

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) Alibaba, Inc. and its affiliates.
3+
# pylint: disable=protected-access
4+
5+
import base64
6+
from http import HTTPStatus
7+
8+
from dashscope.api_entities.dashscope_response import DashScopeAPIResponse
9+
from dashscope.audio.http_tts.http_speech_synthesizer import (
10+
HttpSpeechSynthesizer,
11+
)
12+
13+
14+
def _ok_response(output, request_id="req-1"):
15+
return DashScopeAPIResponse(
16+
status_code=HTTPStatus.OK,
17+
request_id=request_id,
18+
output=output,
19+
)
20+
21+
22+
class TestHandleNonStreamingResponse:
23+
def test_success_proxies_status_code(self):
24+
response = _ok_response(
25+
{
26+
"audio": {
27+
"url": "https://example.com/a.wav",
28+
"id": "audio-1",
29+
"expires_at": 1893456000,
30+
},
31+
},
32+
)
33+
34+
result = HttpSpeechSynthesizer._handle_non_streaming_response(
35+
response,
36+
)
37+
38+
assert result.status_code == HTTPStatus.OK
39+
assert result.request_id == "req-1"
40+
assert result.audio_url == "https://example.com/a.wav"
41+
assert result.audio_id == "audio-1"
42+
assert result.expires_at == 1893456000
43+
44+
def test_failed_response_is_surfaced_not_raised(self):
45+
response = DashScopeAPIResponse(
46+
status_code=HTTPStatus.BAD_REQUEST,
47+
request_id="req-err",
48+
code="InvalidParameter",
49+
message="bad voice",
50+
)
51+
52+
result = HttpSpeechSynthesizer._handle_non_streaming_response(
53+
response,
54+
)
55+
56+
assert result.status_code == HTTPStatus.BAD_REQUEST
57+
assert result.request_id == "req-err"
58+
assert result.code == "InvalidParameter"
59+
assert result.message == "bad voice"
60+
assert result.audio_url is None
61+
62+
63+
class TestHandleStreamingResponse:
64+
def test_chunks_and_final_result_proxy_status_code(self):
65+
audio_bytes = b"fake-audio"
66+
chunks = [
67+
_ok_response(
68+
{
69+
"type": "sentence-begin",
70+
"sentence": {"index": 1},
71+
"audio": {"data": base64.b64encode(audio_bytes)},
72+
},
73+
request_id="req-s1",
74+
),
75+
_ok_response(
76+
{
77+
"finish_reason": "stop",
78+
"audio": {
79+
"url": "https://example.com/a.wav",
80+
"id": "audio-2",
81+
"expires_at": 1893456000,
82+
},
83+
},
84+
request_id="req-s2",
85+
),
86+
]
87+
88+
results = list(
89+
HttpSpeechSynthesizer._handle_streaming_response(iter(chunks)),
90+
)
91+
92+
assert len(results) == 2
93+
assert results[0].audio_data == audio_bytes
94+
assert results[0].status_code == HTTPStatus.OK
95+
assert results[0].request_id == "req-s1"
96+
assert results[1].audio_data == audio_bytes
97+
assert results[1].audio_url == "https://example.com/a.wav"
98+
assert results[1].request_id == "req-s2"
99+
100+
101+
class TestCall:
102+
def test_call_returns_result_with_real_status(self, monkeypatch):
103+
captured = {}
104+
105+
def mock_http_call(cls, **kwargs): # pylint: disable=unused-argument
106+
captured.update(kwargs)
107+
return _ok_response(
108+
{"audio": {"url": "https://example.com/a.wav"}},
109+
)
110+
111+
monkeypatch.setattr(
112+
HttpSpeechSynthesizer,
113+
"_http_call",
114+
classmethod(mock_http_call),
115+
)
116+
117+
result = HttpSpeechSynthesizer.call(
118+
model="cosyvoice-v2",
119+
text="你好世界",
120+
voice="longxiaochun_v2",
121+
volume=None,
122+
rate=None,
123+
pitch=None,
124+
)
125+
126+
assert captured["body"]["input"] == {
127+
"text": "你好世界",
128+
"voice": "longxiaochun_v2",
129+
"format": "wav",
130+
"sample_rate": 24000,
131+
}
132+
assert result.status_code == HTTPStatus.OK
133+
assert result.audio_url == "https://example.com/a.wav"

0 commit comments

Comments
 (0)