Skip to content

feat: rebuild blaze stt and tts on realtime websocket apis - #2389

Open
Devin066 wants to merge 5 commits into
TEN-framework:mainfrom
Devin066:feat/blaze-realtime-asr-tts
Open

Devin066 wants to merge 5 commits into
TEN-framework:mainfrom
Devin066:feat/blaze-realtime-asr-tts

Conversation

@Devin066

@Devin066 Devin066 commented Nov 5, 2026 •

Copy link
Copy Markdown

Summary

Rebuilds the Blaze (blaze.vn) Vietnamese STT and TTS extensions on Blaze's realtime WebSocket APIs and aligns them with the standard extension layout. The previous versions used the batch HTTP APIs (job submit and download), which added several seconds per turn.

blaze_stt_python

Structured like xai_asr_python:

  • config.py, const.py, recognition.py (WebSocket client), reconnect_manager.py, extension.py
  • Streams PCM16 at 16 kHz to wss://api.blaze.vn/v1/stt/realtime (stt-stream-1.5)
  • Partial and final results carry timeline-mapped start_ms/duration_ms, metadata.session_id and vi-VN/en-US language codes
  • Blaze has no flush message, so finalize sends a short trailing silence, waits for the final, and always sends exactly one asr_finalize_end
  • Bounded reconnect with backoff; audio is buffered while disconnected; a bad token is reported once as a fatal error with vendor info

blaze_tts_python

Structured like deepgram_tts, on AsyncTTS2BaseExtension:

  • config.py, blaze_tts.py (WebSocket client), extension.py
  • Streams PCM from wss://api.blaze.vn/v1/tts/realtime (2.0-realtime) over one reused socket, preheated at start
  • Sends TTFB metrics, audio start and end events, and the request-end, interrupted and error end reasons
  • Cancel drops the socket so unread audio never leaks into the next request; failed requests and idle closes reconnect
  • Auth failures (401/403 or close code 1008) are fatal

Shared

  • The endpoint param is base_url, as in other extensions; all properties are declared in the manifests
  • Standard guarder test configs; unit tests run against in-process mock Blaze WebSocket servers
  • New voice_assistant_blaze graph in the voice-assistant example: Vietnamese STT, a Vietnamese LLM prompt and greeting, and Vietnamese TTS
  • Blaze variables added to .env.example

Testing

  • Standalone extension tests (tests/bin/start) in ten_agent_build:0.7.14: STT 75 passed, TTS 46 passed
  • black --line-length 80 clean; pylint shows only the base-class attribute warnings that other extensions (for example fish_asr_python, deepgram_tts) also get
  • ASR guarder against the live Blaze API: 9 passed, 2 skipped; the only failure is test_multi_language, which needs property_zh.json
  • TTS guarder against the live Blaze API: 65 passed, 12 skipped; the only failures are the Google and Cosy hook tests, whose extensions are not installed in a Blaze-only run. test_sequential_requests_ttfb passes with the new Blaze reference.
  • Live check of the extensions: TTS first audio in about 0.45 s; STT returned the exact Vietnamese sentence with finalize ending in 0.39 s
  • End to end in the playground with voice_assistant_blaze: Vietnamese speech in, Vietnamese answer out

ASR design review

  • Package/addon/manifest/graph names match; manifest imports asr-interface.json
  • Config is validated once and logged with config: after redaction
  • Invalid config emits one fatal error and prevents connection attempts
  • start_connection() reports connected only after vendor handshake
  • All connect failures/closures close the base connection state
  • Expected close, unexpected close, stop, and config swap are distinguished
  • Reconnect has one owner, exponential backoff (unlimited non-fatal retries by default), stop/fatal cancellation
  • Stale callbacks from old clients are ignored
  • send/finalize/stop/config swap cannot race the same client
  • Audio buffer policy is explicit, bounded, session-safe, and flushes after recovery
  • AudioFrame buffers are always unlocked and send_audio return value is accurate
  • AudioTimeline includes sent user audio and injected silence exactly once
  • ASR result shape, language, timestamps, words, final/locked semantics are correct
  • session_id is under metadata; vendor fields are under metadata.asr_info
  • Finalize always echoes finalize_id/session_id exactly once, including failures/timeouts
  • Same session works for at least two audio -> finalize cycles
  • Framework error severity and vendor_info are separated correctly
  • Connect delay, TTFW/TTLW, and vendor metrics have correct metadata
  • vendor_metadata and all logs redact secrets and signed URLs
  • Dump contains original PCM without protocol silence contamination
  • Stop/deinit cancels and awaits background tasks; cleanup is idempotent
  • Standalone tests pass
  • ASR guarder passes: all applicable cases pass; test_multi_language needs Chinese, which Blaze does not support
  • black format and pylint checks pass

Notes

  • Blaze does not support Chinese, so there is no property_zh.json and the guarder's multi-language case does not apply.
  • The TTS guarder change only adds a Blaze entry and observer (ttfb_hooks/blaze.py) to the TTFB reference table, with its own tests.

- blaze_stt_python: stream to wss /v1/stt/realtime (stt-stream-1.5),
  laid out like xai_asr_python with reconnect manager, finalize via
  trailing silence, timeline-mapped timestamps and session metadata
- blaze_tts_python: stream PCM from wss /v1/tts/realtime (2.0-realtime)
  on AsyncTTS2BaseExtension, laid out like deepgram_tts, with preheat,
  cancel, TTFB metrics and fatal auth errors
- use base_url for the endpoint param and add standard guarder configs
  and unit tests against in-process mock servers
- add voice_assistant_blaze graph (Vietnamese STT, LLM prompt and TTS)
  and Blaze variables to .env.example
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

Findings

[P1] Do not reconnect from the receive task that must be closed
ai_agents/agents/ten_packages/extension/blaze_stt_python/recognition.py:169 and extension.py:164
On an unexpected socket close, _message_handler awaits on_close, which runs the reconnect loop and calls _connect_recognition. That tries to close the old recognition object; close() cancels and awaits its own _message_task, which is still executing this callback. The task is cancelled before a replacement is established, so recognition remains disconnected after a normal network interruption. Schedule and own the reconnect separately from the receive task, and test a real socket close through recovery.

[P1] Report the ASR connection state transitions
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:263
The handshake callback never calls on_connected(), and neither failed connection attempts nor socket closure call on_disconnected(). The base wrapper reports connecting, but consumers never receive a corresponding usable or disconnected state, including across retries. This violates the ASR connection-state MUST contract and blocks merge. Report transitions at handshake and actual closure/failure, and route new attempts through public start_connection().

[P1] Finalize audio even when Blaze has not emitted a partial
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:187
_has_pending_speech is set only by a nonempty partial, not when user audio is sent. If finalize arrives after audio but before the first partial, or if Blaze sends only a final, this branch immediately acknowledges finalize without sending the endpointing silence or waiting. The utterance can remain unfinalized and its transcript arrive after the caller has moved on. Track unfinalized input independently of partial events and exercise this ordering in an end-to-end finalize test.

[P1] Preserve finalize request identity and serialize concurrent finalizes
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:183
The implementation ignores the finalize session argument and never captures finalize metadata; send_asr_finalize_end() uses the latest audio metadata. A finalize for a different session therefore reports the wrong session. In addition, concurrent calls share last_finalize_timestamp and the base mutable finalize ID: the first completion clears the timestamp and the other emits no end, or uses the overwritten ID. Keep separate finalize contexts or serialize before the base updates its ID, and echo each request ID and session exactly once. This is an ASR MUST requirement.

[P2] Drain retained audio when the connection becomes ready
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:271
The keep-buffer strategy retains frames during an outage, but on_open never flushes them. The base drains the buffer on a subsequent input frame, so if the speaker stops during the outage and no further frame arrives, the buffered utterance is never sent even after recovery. Drain with each frame own session metadata after readiness and test recovery without a subsequent input frame. This is an ASR buffering MUST requirement.

[P2] Record user audio only after the WebSocket send succeeds
ai_agents/agents/ten_packages/extension/blaze_stt_python/recognition.py:175
The timeline advances before websocket.send(). If that send fails, send_audio returns false and the frame can be buffered and resent, yet the failed attempt remains in the timeline. Subsequent result timestamps and actual-send metrics then include audio the vendor never received, potentially counting it twice. Add user audio only after successful handoff and test a failed send followed by retry; this is an ASR timeline MUST requirement.

[P2] Measure the no-audio end interval from the actual start event
ai_agents/agents/ten_packages/extension/blaze_tts_python/extension.py:167
For a vendor error or timeout before the first audio, _process_tts_text has already set sent_ts at request time. _finalize_request emits a start only at termination but computes the interval from that earlier request timestamp, so a 30-second no-audio timeout reports roughly 30 seconds between start and end even though those events are adjacent. Set the start boundary when the event is emitted, including no-audio paths, and assert the interval with a controlled clock.

[P2] Add the required independent Blaze TTFB hook
ai_agents/agents/ten_packages/extension/blaze_tts_python/blaze_tts.py:132
Neither guarder TTFB registry contains blaze_tts_python, so the reference installer returns no observer for this WebSocket protocol. The extension-local metric test only checks the production metric against a delay and cannot independently establish the text-send and first nonempty audio boundaries. Add and register a Blaze observer with protocol and connection-isolation tests, then provide a reference-enabled run.

[P2] Cover TTS terminal accounting and flush at the extension boundary
ai_agents/agents/ten_packages/extension/blaze_tts_python/tests/test_basic.py:41
The local suite asserts successful PCM duration, but does not assert the start-to-end event interval with a controlled clock or exact duration and interval after partial-audio error, flush, cancellation, and no-audio termination. The client cancellation test does not exercise emitted TEN events or request ownership. These are mandatory TTS local-test matrix cases; add deterministic extension-level tests that also check no stale audio/end after flush and that the next request succeeds.

ASR design review

  • Lifecycle: Fail; finalize identity/concurrency and receive-task ownership need correction.
  • Connection state: Fail; connected/disconnected transitions are missing.
  • Buffering: Fail; retained frames are not drained on readiness without a later frame.
  • Finalize: Fail; speech without a prior partial can be acknowledged before draining, and request identity is not preserved.
  • Reconnect: Fail; socket-close recovery cancels its own receive task. The bounded retry policy is a SHOULD deviation and needs documented vendor limits, alternatives, and test evidence.
  • Result shape: Pass for inspected fields; session is nested and empty results are dropped. End-to-end session-echo coverage remains needed.
  • Metrics: Fail; failed sends advance the timeline. Connect-delay emission is present.
  • Tests: Fail; the above lifecycle and recovery orderings are not covered, and the PR states the live ASR guarder has not been run. Please provide guarder results; document the unsupported Chinese case as already noted.

Review is based on the PR diff and checked-out base guidance; I did not execute code or install dependencies from the PR head.

blaze_stt_python, following the ASR design review guide and the
long-lived WebSocket pattern of fish_asr_python:
- run reconnect on its own task with capped backoff and unlimited
  retries; a socket close no longer cancels its own recovery
- report connected after Blaze's ready message and disconnected on every
  connect failure or close; retries go through start_connection()
- ignore callbacks from replaced sockets with a connection generation;
  a normal close emits no error; auth failures stay fatal and final
- finalize on audio sent since the last final, serialize requests and
  echo each request's finalize_id and session in asr_finalize_end
- drain kept audio when a new socket is ready, with per-frame metadata
- add user audio to the timeline only after a successful send
- add connection status, reconnect lifecycle and audio timeline tests,
  including recovery from a real server-side socket close

blaze_tts_python:
- measure the start-to-end interval from the emitted start event when a
  request ends without audio
- add deterministic extension tests for success, partial-audio error,
  no-audio error and flush accounting
- register a Blaze TTFB observer in the TTS guarder reference table
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

Findings

[P1] Preserve newer audio when an earlier final arrives
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:639
Blaze endpointing and audio input run concurrently: after audio for a new utterance has been sent, a delayed final for the preceding utterance can arrive and unconditionally clear _unfinalized_audio. A subsequent asr_finalize then skips silence and immediately acknowledges the newer audio, so its transcript can arrive after finalize or remain unfinalized. This violates the ASR finalize MUST contract and blocks merge. Track finalization against the vendor audio boundary or an equivalent per-utterance watermark, and test this ordering with audio sent before the previous final is received.

[P1] Redact credentials embedded in configured URLs
ai_agents/agents/ten_packages/extension/blaze_stt_python/config.py:83
The URL validator accepts URLs containing user-info or secret query parameters, but to_json(sensitive_handling=True) redacts only sensitive keys, not the value of base_url. The effective config is logged on initialization, and the same URL is also logged in recognition.py and returned as vendor metadata; credentials in a proxy URL or signed URL are therefore exposed in logs and connection-status events. The TTS config similarly logs base_url without sanitizing it. Reject credential-bearing URLs or sanitize them consistently before all logging and metadata output. This violates the ASR secret-redaction MUST contract and blocks merge.

[P2] Preserve buffered frame order after a failed drain
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:443
When a drain has queued frames A and B and sending A fails, A is removed then appended behind B. On the next successful recovery B is sent before A, reversing parts of the retained PCM stream and potentially interchanging session boundaries. Preserve the failed frame at the front along with the rest of the queue, and test a failed first send followed by a successful drain. The ASR buffering MUST contract requires session-safe retained audio, so this blocks merge.

[P2] Ignore empty binary frames when starting TTFB
ai_agents/agents/ten_packages/extension/blaze_tts_python/blaze_tts.py:180
Any binary WebSocket message, including b"", emits the one-shot TTFB event before PCM alignment. If an empty binary message precedes audio, the reported TTFB and tts_audio_start are early; if no audio follows, the request is treated as having started audio at that empty frame. Wait until the first non-empty valid PCM payload before emitting the metric/start, and cover empty frames before audio and before a no-audio end.

[P2] Cover interrupted accounting and connection cancellation at the extension boundary
ai_agents/agents/ten_packages/extension/blaze_tts_python/tests/test_request_accounting.py:168
The new flush test verifies the end reason and subsequent request, but does not assert the interrupted request’s exact emitted-byte duration or controlled start-to-end interval. It also uses a mocked stream rather than exercising cancellation while the real client is connecting or awaiting its first byte. These are required TTS local-test matrix cases: assert both accounting fields against actual emitted PCM with a controlled clock, and exercise cancellation during connection/first-byte wait and subsequent recovery. Please also provide a reference-enabled Blaze TTFB guarder run; the PR currently states that the live guarder has not been run.

ASR design review

  • Lifecycle: Fail; delayed finals can clear newer pending audio.
  • Connection state: Pass for inspected handshake, failure, and close paths.
  • Buffering: Fail; failed drains reorder retained frames.
  • Finalize: Fail; the delayed-final ordering can acknowledge pending speech prematurely.
  • Reconnect: Pass for inspected recovery ownership; the new socket-close recovery test covers the earlier self-cancellation issue.
  • Result shape: Pass for inspected fields; emitted session and vendor fields use metadata.
  • Metrics: Pass for inspected send and connect accounting.
  • Tests: Fail; the above orderings and credential-bearing URL redaction need regression tests, and live ASR guarder results are not yet provided. The PR documents Chinese as unsupported.

Review based on the current PR head and repository guidance; I did not execute or install code from the PR head.

The TTS guarder found that a missing dump directory made the dump flush
raise after tts_audio_end, skipping finish_request(); every later request
then queued behind it. Create the dump directory and log dump flush
errors instead of letting them escape finalization.
@Devin066

Devin066 commented Nov 5, 2026

Copy link
Copy Markdown
Author

Thanks for the review. All findings are addressed in 1a96722, plus 8654c65 for an issue the TTS guarder found. blaze_stt_python now follows docs/ai/L1/L2/asr_plugin_design_review.md and the long-lived WebSocket pattern from fish_asr_python.

[P1] Reconnect from the receive task. Fixed. Recovery runs on its own task (_schedule_reconnect / _reconnect_after_disconnect), and the client never reconnects itself. BlazeSTTRecognition.close() does not cancel the task it is running in. test_reconnect_lifecycle.py::test_real_socket_close_recovers_on_a_new_socket covers a real server-side close (1011) through recovery against an in-process WebSocket server.

[P1] Connection state transitions. Fixed. on_connected() is called after Blaze's ready message, and on_disconnected() on every connect failure and socket close. Retries go through _ensure_connection(), which calls public start_connection(). A connection generation makes callbacks from a replaced socket no-ops. ConnectionClosedOK reports disconnected without an error event. Retries are unlimited with capped backoff (0.5 s to 8 s); auth failures (failed-authentication / 1008) are fatal and never retried. Covered in test_connection_status.py, test_reconnect.py and test_reconnect_lifecycle.py.

[P1] Finalize without a prior partial. Fixed. Pending input is now tracked as user audio sent since the last final, independent of partials (test_finalize.py::test_audio_without_any_partial_is_still_finalized).

[P1] Finalize identity and concurrency. Fixed. on_data snapshots each request's metadata, and finalizes are serialized with a lock. asr_finalize_end echoes that request's own finalize_id and metadata instead of the base class's latest-frame values. Disconnected, timeout and send-failure paths each end exactly once (test_finalize.py, including test_concurrent_finalizes_echo_independent_ids_and_sessions).

[P2] Drain retained audio on readiness. Fixed. Kept frames are drained right after the handshake, each with its own frame metadata, while is_connected() stays False so new frames queue behind them (test_kept_audio_is_drained_when_ready_without_a_new_frame). A finalize while disconnected discards kept audio so it is never replayed into a later turn.

[P2] Timeline after a successful send. Fixed. add_user_audio runs only after send_audio succeeds, and finalize silence is recorded with add_silence_audio and kept out of the dump (test_audio_timeline.py, test_dump.py).

[P2] TTS no-audio interval. Fixed. When a request ends without audio, the start boundary is set when tts_audio_start is emitted, so start and end are adjacent (test_request_accounting.py::test_no_audio_error_has_adjacent_start_and_end, which would have reported 30000 ms before the fix).

[P2] Blaze TTFB hook. Added ttfb_hooks/blaze.py (BlazeObserver) and registered blaze_tts_python in ttfb_reference.py. It measures from a query frame with non-empty query to the first non-empty binary PCM frame, and ignores the auth frame and JSON status events. The endpoint is resolved through BlazeTTSConfig.ws_url to wss://api.blaze.vn/v1/tts/realtime. Tests: test_blaze_ttfb_reference.py (control frames, empty audio, sequential requests on one socket, connection isolation).

[P2] TTS terminal accounting tests. Added test_request_accounting.py. It drives the real extension with a controlled clock and asserts exact interval and duration for success, an error after partial audio, an error with no audio, and flush mid-stream: one INTERRUPTED end, no stale audio after it, and the next request ends with REQUEST_END.

Results

  • Standalone tests: STT 57 passed, TTS 38 passed. Guarder observer tests: 5 passed.
  • Live check against the Blaze API with the extension:
    • STT: connecting then connected, 16 partials and the exact final sentence (vi-VN). Finalize took 0.39 s and echoed finalize_id and session.
    • TTS: first audio in 449 ms.
  • ASR guarder (task asr-guarder-test EXTENSION=blaze_stt_python, live API): 9 passed, 2 skipped, 1 failed. The failure is test_multi_language, which needs property_zh.json (see below). Same-session finalize/reconnect passed with 2 non-empty finals, and invalid credentials give one -1000 error with vendor_info.code "401".
  • TTS guarder (task tts-guarder-test EXTENSION=blaze_tts_python, live API): 65 passed, 12 skipped, 2 failed. Both failures are test_provider_ttfb_hooks.py tests for Google and Cosy, which fail with ModuleNotFoundError because those extensions are not installed in a Blaze-only run. test_sequential_requests_ttfb passes with the new Blaze reference enabled.
  • The first TTS guarder run found a real bug, fixed in 8654c65. A missing dump directory made the dump flush raise after tts_audio_end, skipping finish_request(), so the next request queued forever. The extension now creates the dump directory and logs flush errors instead of letting them escape; test_request_accounting.py covers both cases.

Blaze does not support Chinese, so there is no property_zh.json; the guarder's Chinese case does not apply.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

[P1] Bound the entire STT readiness handshake
ai_agents/agents/ten_packages/extension/blaze_stt_python/recognition.py:99

The timeout is restarted for every message. If a connected server sends progress, unknown, or binary messages more frequently than the timeout but never sends ready, _wait_ready() never returns. During a reconnect, start_connection() holds _lifecycle_lock throughout this wait, while finalize and stop both need that lock, so asr_finalize_end may never be emitted and shutdown can hang. This violates the ASR bounded-finalize MUST and blocks merge. Apply one deadline to the entire readiness handshake and test finalize and stop while the peer keeps sending non-ready messages.

[P2] Treat HTTP authentication failures as fatal on STT connect
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:219

A WebSocket upgrade rejected with HTTP 401 or 403 raises websockets.exceptions.InvalidStatus, not BlazeSTTAuthError. Consequently this path emits a non-fatal error with code InvalidStatus and starts unlimited retries, even though the credentials or authorization cannot recover by retrying. This violates the ASR authentication-error MUST and blocks merge. Classify the upgrade response status as fatal for 401/403, preserve the actual status in vendor_info, and test that no reconnect follows.

ASR design review

  • Lifecycle: Fail; the readiness handshake can block stop and finalize, in addition to the delayed-final issue already reported.
  • Connection state: Pass for inspected handshake and close transitions.
  • Buffering: Fail; the previously reported failed-drain ordering issue remains.
  • Finalize: Fail; readiness can block completion and a delayed final can acknowledge newer audio.
  • Reconnect: Fail; HTTP authentication failures retry indefinitely. Transient retries otherwise use capped backoff without a retry ceiling.
  • Result shape: Pass for inspected fields; session and vendor fields are nested in metadata.
  • Metrics: Pass for inspected send and connect accounting.
  • Tests: Fail; add the two failure-ordering tests above and the regression tests requested in the earlier comment. Live ASR guarder evidence is still absent; the PR documents Chinese as unsupported.

The TTS-specific issues and test gaps from the preceding review remain applicable at this head. Review based on head 8654c6524a72f4c2958f86e5ea3c286925382ed7; no PR-head code was executed or installed.

blaze_stt_python:
- track pending speech with a watermark on the vendor stream clock, so a
  late final for an earlier utterance cannot acknowledge newer speech
- keep sending real-time silence while finalize waits on an idle stream,
  since Blaze endpointing only advances while audio arrives
- bound the whole handshake with one deadline; finalize no longer waits
  on a handshake and stop aborts one in progress
- treat HTTP 401/403 on the upgrade as fatal with the real status
- keep a failed buffered frame first so drained audio stays in order,
  and mark a closed client unusable immediately so recovery runs

blaze_tts_python:
- start TTFB and audio only on the first non-empty, whole-sample PCM
- finalize an interrupted request before closing the socket, so the
  closed socket cannot turn it into an error

both:
- reject base_url values with credentials, a query or a fragment,
  without echoing them
- add tests for each case, including real-client cancellation while
  waiting for the first byte and while reconnecting
@Devin066

Devin066 commented Nov 5, 2026

Copy link
Copy Markdown
Author

Thanks for the follow-up reviews. Both rounds of findings are addressed in b72f30d.

[P1] Delayed final acknowledging newer audio. Fixed with a per-utterance watermark on Blaze's stream clock (user audio plus injected silence, the clock start_ms / end_ms use). A sent frame with speech-level energy records its stream end. Speech is pending while a voiced frame lies beyond the latest final's end_ms (plus 400 ms tolerance), or a partial arrived since that final. A waiting finalize is released only by a final that covers the pending speech, so a late final for an earlier utterance cannot acknowledge newer speech. Tests: test_late_final_for_an_earlier_utterance_does_not_ack_newer_speech (including a repeated stale final during the wait) and test_trailing_silence_after_a_final_needs_no_wait.

[P1] Credentials in configured URLs. base_url with user-info, a query, or a fragment is now rejected by validate_config() in both STT and TTS, so it never reaches logs, vendor metadata, or status events. The error does not echo the URL (test_urls_with_secrets_are_rejected_without_echoing_them).

[P1] Unbounded readiness handshake. One deadline now covers connect, auth, and ready (test_handshake_has_one_overall_deadline, where the peer keeps sending progress and binary frames). Finalize no longer takes the lifecycle lock, so a handshake in progress cannot delay asr_finalize_end, and stop_connection() aborts an in-progress handshake (test_finalize_and_stop_do_not_wait_for_a_stalled_handshake, against a stalling server).

[P2] HTTP 401/403 on the upgrade. InvalidStatus with 401 or 403 now raises the fatal auth error with the real status in vendor_info.code, and no reconnect follows (test_http_auth_rejection_raises_auth_error_with_status, test_http_forbidden_upgrade_is_fatal_with_its_status).

[P2] Failed drain order. A failed send now keeps that frame first, followed by the rest in order, with buffered_frames_size recomputed. This test also found a related bug: after a send failure, the old client still reported connected, so recovery thought the link was live. A handled close now marks the client unusable immediately (test_failed_drain_keeps_kept_audio_in_order, which also checks that recovery replays A, B, C in order on the new socket).

[P2] Empty binary frames and TTFB. TTFB and audio start now fire only on the first non-empty, whole-sample PCM payload (test_empty_and_half_sample_frames_do_not_start_ttfb, test_empty_frames_with_no_audio_never_report_ttfb).

[P2] Interrupted accounting and real-client cancellation. The flush test now asserts the interrupted request's exact request_total_audio_duration_ms and request_event_interval_ms against the PCM actually emitted, with a controlled clock. test_real_client_cancel_while_waiting_and_connecting_then_recovers runs the real client against a mock Blaze server. It flushes one request while the client waits for its first byte and another while it is reconnecting, then a third request ends with REQUEST_END. Both interrupted requests end with zero audio and no error event. Writing it showed that cancel_tts() closed the socket before marking the request interrupted, so the request task could finalize it as ERROR; it now finalizes first.

Also found by the live check. On some runs Blaze needed more than the initial 800 ms of finalize silence. Once the input stream went idle its endpointing stopped advancing and no final arrived. While finalize waits on an idle stream, the extension now keeps sending real-time silence in 100 ms steps, and adds nothing while user audio still flows (test_idle_stream_keeps_receiving_silence_until_the_final, test_flowing_user_audio_gets_no_extra_silence_while_waiting).

Results

  • Standalone tests: STT 70 passed, TTS 41 passed. black clean; pylint shows only the base-class attribute warnings other extensions also get.
  • Live check against the Blaze API, 5 runs: the exact Vietnamese final every time, with finalize taking 0.37 to 0.82 s.
  • ASR guarder (live API, this head): 9 passed, 2 skipped. The only failure is test_multi_language, because Blaze has no Chinese and so no property_zh.json.
  • TTS guarder (live API, this head): 65 passed, 12 skipped. test_sequential_requests_ttfb passes with the Blaze reference enabled. The only failures are the Google and Cosy hook tests (ModuleNotFoundError, since those extensions are not installed in a Blaze-only run).

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

[P1] Cancel the in-progress WebSocket open, not just the current socket
ai_agents/agents/ten_packages/extension/blaze_tts_python/blaze_tts.py:140

If a flush arrives while websockets.connect() is still pending, _ws has not yet been assigned, so cancel()/_close() has nothing to close. The old get() subsequently finishes connecting, clears _is_cancelled at line 147, and sends the interrupted query after the flush; a subsequent request can also race with that connection and share or replace its socket. This violates the flush boundary and can mix request audio. Track and cancel the opening operation or use a generation check before installing the socket and before sending the query. Add a regression that blocks the actual WebSocket open (the current cancellation test blocks the later greeting).

[P1] Validate or redact the URL before logging the TTS configuration
ai_agents/agents/ten_packages/extension/blaze_tts_python/extension.py:65

to_str(sensitive_handling=True) masks api_key but leaves base_url and other params unchanged, and this log runs before validate_config(). Consequently, a configured URL such as https://user:password@host or a signed query URL is printed in plaintext even though validation rejects it immediately afterward. Validate before logging and redact URL credentials, query strings, and sensitive pass-through parameters in all configuration representations.

[P1] Preserve session ownership when draining buffered ASR audio (merge blocker)
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:497

When an outage buffers frames from sessions A and B, the drain sends both into the same vendor stream and repeatedly overwrites self.metadata/self.session_id. Blaze results arrive asynchronously and _build_metadata() reads only the latest metadata, so a delayed result for A is emitted as B; the vendor may also combine their audio into one utterance. Reading metadata per frame is not sufficient to preserve ownership. Separate the streams by session or explicitly prevent cross-session replay and preserve the owner until its results are resolved; test emitted result JSON with delayed results and multiple buffered sessions. This violates the ASR buffering and result-session MUST rules.

[P1] Do not let the coverage tolerance acknowledge new speech (merge blocker)
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:478

After a final ending at 1000 ms, a new voiced frame ending at 1300 ms is treated as already covered because it falls inside the 400 ms tolerance. If finalize arrives before Blaze's first partial for that new utterance, _has_pending_speech() returns false, so finalize sends neither silence nor a wait and immediately emits asr_finalize_end; Blaze endpointing may never advance to produce that turn's final once input goes idle. Track speech after the previous final independently of tolerance, and test a short second turn finalized before its first partial. This violates the finalize/result ordering MUST contract for speech-bearing input.

[P2] Cover interleaved requests and replacement-connection cancellation
ai_agents/agents/ten_packages/extension/blaze_tts_python/tests/test_state_machine.py:19

The extension-local suite covers sequential requests and appended fragments, but not rapidly interleaved request IDs or concurrent cancellation against a replacement connection. Those are mandatory TTS transport and request-ownership cases, and the implementation has shared request and socket state. Add deterministic tests that interleave fragments for two IDs and hold an old connection open while cancellation and a new request overlap, asserting audio ownership, exactly-once ends, and that the replacement remains usable.

ASR checklist

  • Lifecycle: pass on inspected paths; connection state: pass on inspected paths.
  • Buffering: fail, cross-session replay/result attribution above.
  • Finalize: fail, short subsequent speech can be acknowledged prematurely above.
  • Reconnect: pass on inspected paths; result shape: fail for cross-session attribution.
  • Metrics: no specific defect found in the inspected paths.
  • Tests: fail for the missing boundary cases above; the latest author-reported ASR guarder run also has one failing multilingual test (Chinese is reported unsupported). Standalone and live tests were not run by this review.

- stt: keep-buffer holds one session; a newer session supersedes kept audio
- stt: label results by the session that owned that span of the stream
- stt: end-time tolerance only covers speech sent before the final arrived
- stt: abort a stalled handshake instead of waiting for close_timeout
- tts: cancel invalidates a socket still opening or reconnecting
- tts: a cancelled stream only discards its own socket
- tts: validate config before logging; redact url parts and secret params
@Devin066

Devin066 commented Nov 5, 2026

Copy link
Copy Markdown
Author

Thanks for the third review. All findings are addressed in 9e69c42.

[P1] TTS flush while the socket is opening. cancel() and stop() now bump a client generation. get() records it before connecting and checks it after connecting and after a reconnect inside the send path, so a request cancelled while websockets.connect() or the handshake is pending never sends its query. The handshake now runs on a local socket and is installed only if its generation is still current; otherwise the new socket is closed. Regressions: test_cancel_while_the_socket_is_opening_sends_nothing and test_cancel_while_reconnecting_a_closed_socket_sends_nothing hold the real websockets.connect() open, flush, then release it. Both fail on the previous head.

While writing the overlap test I found a related bug. A cancelled stream that hit ConnectionClosed cleared self._ws unconditionally, so it could drop the socket a newer request had just opened. Each stream now reads from and discards only its own socket (test_cancelled_stream_cannot_close_the_next_requests_socket also checks that a third request reuses the second socket).

[P1] TTS config logged before validation. validate_config() now runs before the config: log. to_str() now also strips user-info, query, and fragment from base_url and encrypts any params key that looks sensitive (test_config_log_redacts_url_parts_and_sensitive_params).

[P1] STT drain across sessions. Two parts:

  • The keep-buffer now holds one session, following the fish_asr_python pattern. The extension owns _handle_audio_frame, so a frame from a newer session drops kept audio from an older one instead of replaying both into one Blaze stream. The drain and the next live frame are serialized under the audio lock.
  • Results are labelled by stream position. Each socket records which frame metadata owns each span of its stream clock, and a partial or final takes the owner at its start_ms. send_asr_result() merges self.metadata into the result, so the owner is made current for that call and then restored. A late result for session A keeps A's label after B's audio has started.

Tests check the emitted JSON: test_kept_audio_of_an_older_session_is_not_replayed_for_a_newer_one, test_late_result_keeps_the_session_whose_audio_produced_it, test_buffered_session_then_live_session_results_are_labelled_by_audio.

[P1] Tolerance acknowledging new short speech. The 400 ms end-time tolerance now applies only to voiced frames sent before the latest final arrived. Voiced audio sent after a final is tracked separately and stays pending until a final's end_ms actually covers it, so a repeated stale final cannot release it either. Tests: test_short_turn_after_a_final_waits_for_its_own_final (a short second turn finalized before its first partial) and test_stale_final_does_not_cover_speech_sent_after_a_final. Live runs show why the tolerance is still needed for earlier speech: Blaze's final end_ms landed up to 220 ms before the last voiced frame.

[P2] TTS coverage. Added test_interleaved_request_ids_stay_separate (three fragments across two request IDs: order, start/end pairing, per-request duration) and the overlap test above.

Also fixed. stop_connection() during a stalled handshake could wait the full websockets close_timeout (10 s) for a close handshake the stalled peer never completed. An aborted handshake now aborts the transport.

Results

  • Standalone tests: STT 75 passed, TTS 46 passed. Of the new regression tests, 8 fail against the previous head. black clean; pylint shows only the base-class attribute warnings other extensions also get.
  • Live check against the Blaze API, 5 runs through _handle_audio_frame: the exact Vietnamese final every time, finalize taking 0.35 to 0.58 s.
  • ASR guarder: 9 passed. test_multi_language fails because Blaze is Vietnamese-only.
  • TTS guarder: 65 passed. The only failures are test_google_public_lazy_iterator and test_cosy_hook_observes_sdk_pool_reuse, which fail on imports unrelated to this extension.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

[P1] Keep different sessions out of the same Blaze utterance (merge blocker)
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:455

The single-session policy only applies while frames are buffered. When connected, audio for session B is sent on the same Blaze stream immediately after session A, even if A has not finalized. Blaze can return one final spanning both sessions; assigning its start_ms owner labels the entire combined transcript as A, leaking B's speech into A and leaving B without its own result. The current tests simulate two separate finals, so they do not cover this. Establish a vendor utterance or connection boundary on a live session change, or otherwise prevent cross-session audio from being combined. This violates the ASR session-ownership MUST.

[P1] Do not let a delayed final cover a short newer utterance (merge blocker)
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:588

If A's final ending at 100 ms is delayed until after B's voiced 100–200 ms frame was sent, _voiced_end_after_final_ms is still unset because no final had arrived when B was sent. The 400 ms tolerance then treats B as covered by A's final, so B's finalize immediately acknowledges without silence or waiting for B's final. The regression test uses an 800 ms gap and therefore misses this boundary. Track utterance ownership independently of the tolerance, and test a delayed A final with a short B turn inside that tolerance. This violates the ASR finalize/result ordering MUST.

[P1] Do not classify low-level speech as an already finalized turn (merge blocker)
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:531

The only indication of pending audio before a partial is the fixed mean-amplitude threshold of 300. For valid quiet PCM below that threshold, a finalize arriving before Blaze's first partial takes the no-pending-speech branch, sends no endpointing silence, and emits asr_finalize_end immediately; a recognizable utterance can therefore be lost or delivered after the caller proceeds. Track sent, unfinalized user audio independently of this local energy heuristic and cover quiet speech before the first partial. The ASR contract requires a non-empty final for speech-bearing finalize cycles.

[P1] Serialize audio emission with the TTS flush boundary
ai_agents/agents/ten_packages/extension/blaze_tts_python/extension.py:319

With dumping enabled, flush can run while _write_dump() is awaited after the chunk has been counted but before send_tts_audio_data(). cancel_tts() emits the interrupted end and completes the flush, then this coroutine resumes and sends the old chunk after that end; the reported duration also includes PCM that had not been emitted at the boundary. The finalized check at the top of the loop is too early to prevent this ordering. Serialize emission and terminal handling, or revalidate ownership immediately before emission and count only successfully emitted bytes; add a gated dump-write/flush regression.

[P2] Attribute partials to the active utterance, not the previous final
ai_agents/agents/ten_packages/extension/blaze_stt_python/extension.py:827

After session A's final ends at 100 ms, endpointing silence can advance the stream to 900 ms before session B starts. The first B partial has no timestamp, but this code looks up the owner at _last_final_end_ms (100 ms), which is still A, so it emits B's text with A's metadata.session_id and an outdated start time. Track the owner/start of the current utterance, including injected silence and session changes, and assert emitted partial JSON across consecutive sessions. Incorrect session metadata violates the ASR result MUST and blocks merge.

[P2] Classify transient preheat failures as recoverable
ai_agents/agents/ten_packages/extension/blaze_tts_python/extension.py:76

An HTTP 429 or 5xx during the WebSocket upgrade becomes BlazeTTSConnectionException, escapes client.start(), and is always emitted here as FATAL_ERROR. The same status is correctly classified non-fatal during a later request. A temporary outage during preheat is therefore reported as unrecoverable and can cause the session to be torn down instead of recovering on a subsequent request. Reserve fatal initialization errors for configuration and auth failures, and test a transient upgrade failure followed by a successful request.

ASR checklist

  • Lifecycle: fail, live session changes are not isolated.
  • Connection state: pass on inspected handshake and close paths.
  • Buffering: fail, session isolation is not maintained when live audio resumes.
  • Finalize: fail, delayed finals and low-level speech can be acknowledged prematurely.
  • Reconnect: pass on inspected retry and cancellation paths.
  • Result shape: fail, partials can carry the previous session's metadata.
  • Metrics: pass on inspected connect and successfully sent audio accounting.
  • Tests: fail for the boundary cases above. The author reports one ASR guarder multilingual failure and documents Chinese as unsupported.

Reviewed against head 9e69c42ffc77aae737313000b44231cb96905406. The TTS extension directory exists on the base branch, so the new-extension screenshot/PCM evidence gate does not apply. I did not run or install PR-head code.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant