Skip to content

fix(model): declare real WAV sizes in the final streamed audio block - #3242

Merged
qbc2016 merged 2 commits into
agentscope-ai:mainfrom
BlueX888:fix/prep-streamed-audio-final-wav-placeholder-header
Oct 12, 2026
Merged

qbc2016 merged 2 commits into
agentscope-ai:mainfrom
BlueX888:fix/prep-streamed-audio-final-wav-placeholder-header

Conversation

@BlueX888

@BlueX888 BlueX888 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

AgentScope Version

2.0.10dev (main @ e4c1e07)

Description

Summary

In streaming mode, the final is_last audio DataBlock kept the streaming WAV header's 0xFFFFFFFF RIFF/data placeholder sizes instead of the real chunk sizes. The completed audio block is now finalized into a well-formed WAV, matching what the non-streaming path produces for the same call.

Why

  • _build_streaming_wav_header() (src/agentscope/_utils/_audio.py:47-73) deliberately sets the RIFF and data chunk sizes to 0xFFFFFFFF since the total length isn't known yet, and its docstring states: "For a complete buffer use _build_wav_header instead, which declares real chunk sizes" (src/agentscope/_utils/_audio.py:64-65).
  • Both streaming parsers prefix only the first audio delta with that header (src/agentscope/model/_openai_chat/_model.py:412, src/agentscope/model/_dashscope/_model.py:412). The stream accumulator then joins the deltas verbatim into the final is_last block via _AccBase64Source.build(), so the completed artifact kept the placeholder sizes.
  • The non-streaming path already wraps the payload in a real header (src/agentscope/model/_openai_chat/_model.py:519, fix(model): wrap non-streaming chat audio in a real WAV header #3028), so streaming and non-streaming responses disagreed: consumers of the final block that trust the header — wave.getnframes(), duration, seeking, strict players — saw 4294967295 instead of the actual audio length.

Changes

  • src/agentscope/_utils/_audio.py: add _finalize_streaming_wav(), which rewrites bytes 4-8 and 40-44 of a complete streaming buffer with the real RIFF (36 + data_len) and data (data_len) sizes. Buffers without the placeholder header are returned unchanged, so complete WAVs and other audio payloads are untouched.
  • src/agentscope/model/_utils.py: _AccBase64Source.build() finalizes the joined buffer when media_type == "audio/wav". This is the single place where streamed audio deltas are joined for the final response, so both the OpenAI Chat and DashScope stream parsers are covered.
  • The live per-delta chunks are unchanged — only the final accumulated block is affected.

Testing

  • New regression tests TestOpenAIChatStream::test_stream_audio_final_block_declares_real_wav_sizes (tests/model_openai_chat_test.py) and TestDashScopeStream::test_stream_audio_final_block_declares_real_wav_sizes (tests/model_dashscope_test.py). They fail on unpatched main:
>       self.assertEqual(riff_size, 36 + len(pcm_full))
E       AssertionError: 4294967295 != 48
FAILED tests/model_openai_chat_test.py::TestOpenAIChatStream::test_stream_audio_final_block_declares_real_wav_sizes
FAILED tests/model_dashscope_test.py::TestDashScopeStream::test_stream_audio_final_block_declares_real_wav_sizes
============================== 2 failed in 10.11s ==============================

and pass with this change (2 passed); each test also parses the final block end-to-end with wave.open and asserts getnframes().

  • Module tests: python3 -m pytest tests/model_openai_chat_test.py tests/model_dashscope_test.py → 39 passed.
  • Full suite: python3 -m pytest tests (excluding the four files that fail to import without optional extras: blob_store_s3, mem0_agentscope_adapter, tui, webrtc_audio_transport) → 2457 passed, 159 failed; the 159 failures are byte-identical to the same run on unpatched main (they require external services: MySQL, Qdrant, MongoDB, Ollama, DingTalk).
  • pre-commit run --all-files → all hooks Passed (mypy, black, flake8, pylint included).

Checklist

  • An issue has been created for this PR
  • I have read the CONTRIBUTING.md
  • Docstrings are in Google style
  • N/A — internal artifact fix; the streamed block's container format is not described in the documentation repository
  • Code is ready for review

Fixes #3245

The first audio delta of a streamed voice response carries the
streaming WAV header, whose RIFF and data chunk sizes are the
0xFFFFFFFF placeholder. The accumulated final is_last block used to
keep that placeholder, so consumers trusting the header (e.g.
wave.getnframes, duration, seeking) saw garbage instead of the real
audio length, unlike the non-streaming path which wraps the payload
in a real header.

Finalize the WAV header when the accumulator joins the stream
deltas, so the completed audio block is a well-formed WAV.
@github-actions github-actions Bot added pr/awaiting-review Waiting for maintainers to review this PR pr/no-linked-issue This PR does not link to any issue labels Oct 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the pull request. One thing worth sorting out before it is reviewed:

  • This pull request does not close an issue. Please open one describing the problem first and link it with Fixes #<number>, so the change can be discussed against the problem it solves.

If you think it belongs here anyway, say so and a maintainer will take a look.

@github-actions github-actions Bot removed the pr/no-linked-issue This PR does not link to any issue label Oct 11, 2026

@qbc2016 qbc2016 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@github-actions github-actions Bot removed the pr/awaiting-review Waiting for maintainers to review this PR label Oct 12, 2026
@qbc2016
qbc2016 merged commit e941f94 into agentscope-ai:main Oct 12, 2026
9 checks passed
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.

Streaming audio: final audio block keeps the streaming WAV header's 0xFFFFFFFF placeholder sizes

2 participants