Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def __init__(self, name: str) -> None:
self.recorder_map: dict[str, PCMWriter] = {}
# Timestamp when TTS request was sent to service
self.request_start_ts: datetime | None = None
# Timestamp when the first audio chunk was received
self.first_chunk_ts: datetime | None = None
# Total audio duration for current request in milliseconds
self.request_total_audio_duration_ms: int | None = None
# Time to first byte for current request in milliseconds
Expand Down Expand Up @@ -196,6 +198,7 @@ async def request_tts(self, t: TTSTextInput) -> None:
self.total_audio_bytes = 0 # Reset for new request
self.request_ttfb = None
self.first_chunk = True
self.first_chunk_ts = None
self.chunk_count = 0
self.request_start_ts = datetime.now()
self.is_first_message_of_request = True
Expand Down Expand Up @@ -534,6 +537,8 @@ async def _handle_first_audio_chunk(self) -> None:
4. Logs the operation
"""
if self.request_start_ts:
self.first_chunk_ts = datetime.now()

await self.send_tts_audio_start(
self.current_request_id,
)
Expand Down Expand Up @@ -573,9 +578,12 @@ async def _handle_tts_audio_end(
self.total_audio_bytes, self.config.sample_rate
)
)
request_event_interval = int(
(datetime.now() - self.request_start_ts).total_seconds() * 1000
)
request_event_interval = 0
if self.first_chunk_ts is not None:
request_event_interval = int(
(datetime.now() - self.first_chunk_ts).total_seconds()
* 1000
)

# Flush PCMWriter for current request to ensure dump file is written
if (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "extension",
"name": "cosy_tts_python",
"version": "0.4.2",
"version": "0.4.3",
"dependencies": [
{
"type": "system",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cosy-tts-python"
version = "0.4.2"
version = "0.4.3"
requires-python = ">=3.10"
dependencies = [
"dashscope>=1.26.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(self):
self.ttfb_value = -1
self.audio_frame_received = False
self.audio_end_received = False
self.request_event_interval_ms = -1

def on_start(self, ten_env_tester: TenEnvTester) -> None:
"""Called when test starts, sends a TTS request."""
Expand Down Expand Up @@ -61,6 +62,11 @@ def on_data(self, ten_env: TenEnvTester, data) -> None:

elif name == "tts_audio_end":
self.audio_end_received = True
json_str, _ = data.get_property_to_json(None)
payload = json.loads(json_str)
self.request_event_interval_ms = payload.get(
"request_event_interval_ms", -1
)
# Stop the test only after both TTFB and audio end are received
if self.ttfb_received:
ten_env.log_info("Received tts_audio_end, stopping test.")
Expand Down Expand Up @@ -98,7 +104,7 @@ async def simulate_audio_stream():
"""Simulate TTS service sending data asynchronously"""
queue = stream_state["queue"]
# Delay to simulate network latency and TTS processing time
await asyncio.sleep(0.25) # 250ms TTFB
await asyncio.sleep(0.6) # 600ms TTFB

# Send first audio chunk
await queue.put((False, MESSAGE_TYPE_PCM, b"\x11\x22\x33"))
Expand Down Expand Up @@ -139,11 +145,18 @@ async def simulate_audio_stream():
assert tester.audio_frame_received, "Did not receive any audio frame."
assert tester.audio_end_received, "Did not receive the tts_audio_end event."
assert tester.ttfb_received, "TTFB metric was not received."
assert (
tester.request_event_interval_ms >= 0
), "tts_audio_end did not include request_event_interval_ms."

# Check if the TTFB value is reasonable. It should be around 250ms with the delay
# we introduced. Allow 50ms margin for timing variations and system scheduling.
# Check if the TTFB value is reasonable. The larger delay keeps it well
# above the post-first-chunk interval despite scheduling variability.
assert (
tester.ttfb_value >= 200
), f"Expected TTFB to be >= 200ms, but got {tester.ttfb_value}ms."
tester.ttfb_value >= 500
), f"Expected TTFB to be >= 500ms, but got {tester.ttfb_value}ms."
assert tester.request_event_interval_ms < tester.ttfb_value, (
"request_event_interval_ms should start at the first audio chunk and "
"exclude TTFB."
)

print(f"✅ TTFB metric test passed. Received TTFB: {tester.ttfb_value}ms.")
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class ExtensionTesterRobustness(ExtensionTester):
def __init__(self):
super().__init__()
self.first_request_error: Optional[dict[str, Any]] = None
self.first_request_event_interval_ms: Optional[int] = None
self.second_request_successful = False
self.ten_env: Optional[TenEnvTester] = None

Expand Down Expand Up @@ -75,6 +76,14 @@ def on_data(self, ten_env: TenEnvTester, data) -> None:
# After receiving the error for the first request, immediately send the second one.
self.send_second_request()

elif (
name == "tts_audio_end"
and payload.get("request_id") == "tts_request_to_fail"
):
self.first_request_event_interval_ms = payload.get(
"request_event_interval_ms"
)

elif (
name == "tts_audio_end"
and payload.get("request_id") == "tts_request_to_succeed"
Expand Down Expand Up @@ -181,6 +190,10 @@ async def get_audio_data(self):
assert (
tester.first_request_error.get("code") == 1000
), f"Expected error code 1000 (NON_FATAL_ERROR), got {tester.first_request_error.get('code')}"
assert tester.first_request_event_interval_ms == 0, (
"A request with no audio should report a zero event interval, got "
f"{tester.first_request_event_interval_ms}ms"
)

# 2. Verify that vendor_info was included in the error
vendor_info = tester.first_request_error.get("vendor_info")
Expand Down
Loading