Conversation
The weighted fusion counted a candidate that one channel did not retrieve as scoring 0.0 in that channel. A missing result means the channel never scored it, not that it scored worst, and the conflation made the sparse channel unable to contribute any unique result: with the default dense_weight=0.9 a sparse-only candidate could score at most 0.1, below the dense channel's mid-field, so exact FTS5 matches for rare tokens (e.g. case variants the embedding misses) fell out of the fused top-k entirely. Only weight channels that actually retrieved the candidate: the fused score is the weight-normalized average over the channels that scored it. Candidates retrieved by both channels keep their exact previous scores, and dense preference when both channels disagree is unchanged. Fixes AstrBotDevs#9868
With stream=True MiniMax returns a server-side ffmpeg streamed WAV whose RIFF and data chunk sizes are 0xFFFFFFFF placeholders, because the total length is unknowable while streaming. _audio_play concatenated the hex chunks and get_audio wrote the bytes verbatim, so every saved .wav had an invalid header. Browsers tolerate it, but strict decoders (Android WebView, system players) reject the file with no error anywhere in the AstrBot logs. Walk the chunk list after assembly and rebuild the RIFF and data sizes from the actual bytes; files whose header is already consistent, and non-WAV payloads, pass through unchanged. Fixes AstrBotDevs#9860
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="astrbot/core/provider/sources/minimax_tts_api_source.py" line_range="49-62" />
<code_context>
+ while pos + 8 <= len(audio):
</code_context>
<issue_to_address>
**issue (broader_impact):** When an inconsistent WAV contains any chunk besides `fmt ` and `data` (for example `LIST`, `fact`, `JUNK`, or a trailing chunk), `_repair_wav_header` discards those chunks and rebuilds a file containing only `fmt ` and `data`. The resulting audio loses valid metadata and can change the container layout even though only the RIFF and data lengths were intended to be repaired.
**Triggers:** When MiniMax or a compatible WAV producer includes ancillary chunks or chunks after `data`.
**Suggested fix:** Preserve the original bytes and update only the RIFF size field and the selected `data` chunk size, including required chunk padding in the rebuilt RIFF length.
```suggestion
repaired = bytearray(audio)
struct.pack_into("<I", repaired, 4, len(repaired) - 8)
struct.pack_into("<I", repaired, data_start - 4, data_size)
return bytes(repaired)
```
</issue_to_address>
### Comment 2
<location path="astrbot/core/provider/sources/minimax_tts_api_source.py" line_range="36-38" />
<code_context>
+ while pos + 8 <= len(audio):
+ cid = audio[pos : pos + 4]
+ csize = struct.unpack_from("<I", audio, pos + 4)[0]
+ chunk_start, remaining = pos + 8, max(0, len(audio) - (pos + 8))
+ actual = min(csize, remaining)
+ if cid == b"fmt ":
+ fmt = audio[chunk_start : chunk_start + actual]
+ elif cid == b"data":
+ data_start, data_size = chunk_start, actual
+ break
+ if csize > remaining:
+ break
</code_context>
<issue_to_address>
**issue (bug_risk):** A truncated WAV with a complete `fmt ` chunk and a `data` chunk whose declared size exceeds the available payload is treated as valid audio: `actual = min(csize, remaining)` records the short payload, and the function then rewrites the header to advertise that shortened size. This converts an incomplete response into a different apparently consistent WAV instead of passing the truncated bytes through or rejecting them.
**Triggers:** When the streamed response ends before the declared `data` payload is complete.
**Suggested fix:** Detect `csize > remaining` for the `data` chunk and return the original bytes (or raise an explicit truncation error) rather than repairing it.
```suggestion
elif cid == b"data":
if csize > remaining:
return audio
data_start, data_size = chunk_start, csize
break
```
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and if the WAV parser or revised fusion weighting is wrong, users may receive malformed audio or less relevant search results, and repaired temporary audio files may remain after a revert. The affected files are bounded and can be regenerated or cleared, so reverting restores future behavior but does not automatically undo already generated outputs.
Blocking findings: astrbot/core/provider/sources/minimax_tts_api_source.py:62, astrbot/core/provider/sources/minimax_tts_api_source.py:38
| payload = audio[data_start : data_start + data_size] | ||
| return b"".join( | ||
| ( | ||
| b"RIFF", | ||
| struct.pack("<I", 4 + (8 + len(fmt)) + (8 + len(payload))), | ||
| b"WAVE", | ||
| b"fmt ", | ||
| struct.pack("<I", len(fmt)), | ||
| fmt, | ||
| b"data", | ||
| struct.pack("<I", len(payload)), | ||
| payload, | ||
| ) | ||
| ) |
There was a problem hiding this comment.
issue (broader_impact): When an inconsistent WAV contains any chunk besides fmt and data (for example LIST, fact, JUNK, or a trailing chunk), _repair_wav_header discards those chunks and rebuilds a file containing only fmt and data. The resulting audio loses valid metadata and can change the container layout even though only the RIFF and data lengths were intended to be repaired.
Triggers: When MiniMax or a compatible WAV producer includes ancillary chunks or chunks after data.
Suggested fix: Preserve the original bytes and update only the RIFF size field and the selected data chunk size, including required chunk padding in the rebuilt RIFF length.
| payload = audio[data_start : data_start + data_size] | |
| return b"".join( | |
| ( | |
| b"RIFF", | |
| struct.pack("<I", 4 + (8 + len(fmt)) + (8 + len(payload))), | |
| b"WAVE", | |
| b"fmt ", | |
| struct.pack("<I", len(fmt)), | |
| fmt, | |
| b"data", | |
| struct.pack("<I", len(payload)), | |
| payload, | |
| ) | |
| ) | |
| repaired = bytearray(audio) | |
| struct.pack_into("<I", repaired, 4, len(repaired) - 8) | |
| struct.pack_into("<I", repaired, data_start - 4, data_size) | |
| return bytes(repaired) |
| elif cid == b"data": | ||
| data_start, data_size = chunk_start, actual | ||
| break |
There was a problem hiding this comment.
issue (bug_risk): A truncated WAV with a complete fmt chunk and a data chunk whose declared size exceeds the available payload is treated as valid audio: actual = min(csize, remaining) records the short payload, and the function then rewrites the header to advertise that shortened size. This converts an incomplete response into a different apparently consistent WAV instead of passing the truncated bytes through or rejecting them.
Triggers: When the streamed response ends before the declared data payload is complete.
Suggested fix: Detect csize > remaining for the data chunk and return the original bytes (or raise an explicit truncation error) rather than repairing it.
| elif cid == b"data": | |
| data_start, data_size = chunk_start, actual | |
| break | |
| elif cid == b"data": | |
| if csize > remaining: | |
| return audio | |
| data_start, data_size = chunk_start, csize | |
| break |
Fixes #9860
What Changed
minimax_tts_api_source.pygains a module-level_repair_wav_header()helper, and_audio_play()now runs its assembled output through it before the file is written:datachunk size are rebuilt from the actual bytes.Why
With
stream: trueMiniMax returns a server-side ffmpeg streamed WAV; the total length is unknowable while streaming, so both the RIFF size and thedatachunk size are0xFFFFFFFFplaceholders._audio_play()concatenated the hex chunks andget_audio()wrote them verbatim, so everyminimax_tts_api_*.wavshipped an invalid container header —RIFFat offset 4 readsFF FF FF FFand ffprobe warnsIgnoring maximum wav data size, file may be invalid. Browser decoders are tolerant, so this surfaced only as silent playback failures in strict environments (Android WebView, system players), with no error anywhere in AstrBot logs. Prior fix #7797 corrected the container format (MP3→WAV mismatch) but not the streamed header placeholder.Testing
New
tests/test_minimax_tts_wav_header.py:test_placeholder_streamed_header_is_repaired— a0xFFFFFFFF-placeholder WAV is rebuilt with exact RIFF/data sizes, preservingfmtand PCM payload.test_consistent_header_is_left_untouched— a header that already matches the real sizes is returned unchanged (idempotence for non-streamed output).test_non_wav_bytes_pass_through— empty/mp3-ish/truncated inputs are never mangled.test_audio_play_repairs_streamed_wav— end-to-end through_audio_play()with hex-chunked SSE input.All four fail on unpatched
master(95e98b8 — the helper does not exist) and pass with the fix:Summary by Sourcery
Fix streamed MiniMax audio container validity and ensure rank fusion weights only the retrieval channels that returned each candidate.
Bug Fixes:
Tests: