Diff
1diff --git a/internal/server/handlers.go b/internal/server/handlers.go
2index b913a6150db64828f985b6b7eb2e03c7ad65c3f7..c92f3f8ba783ee4e1b21b00b083267391e2f2bfe 100644
3--- a/internal/server/handlers.go
4+++ b/internal/server/handlers.go
5@@ -3,6 +3,7 @@ package server
6 import (
7 "context"
8 "encoding/base64"
9+ "encoding/binary"
10 "encoding/json"
11 "errors"
12 "io"
13@@ -177,6 +178,7 @@ func (s *Server) postSpeech(w http.ResponseWriter, r *http.Request) {
14 writeError(w, http.StatusBadRequest, "not a WAV file")
15 return
16 }
17+ normalizeWav(wav)
18
19 sess.mu.Lock()
20 defer sess.mu.Unlock()
21@@ -187,6 +189,28 @@ func (s *Server) postSpeech(w http.ResponseWriter, r *http.Request) {
22 finishTurn(w, sess, sess.orch.FinishSpeak(ctx))
23 }
24
25+// normalizeWav repairs header sizes that AVAudioRecorder leaves stale: it
26+// preallocates a padded header, streams PCM after it, and never updates the
27+// RIFF size or the data chunk size (written as 0). Strict readers reject such
28+// files.
29+func normalizeWav(wav []byte) {
30+ off := 12
31+ for off+8 <= len(wav) {
32+ id := string(wav[off : off+4])
33+ size := int(binary.LittleEndian.Uint32(wav[off+4 : off+8]))
34+ if id == "data" {
35+ if size != len(wav)-off-8 {
36+ binary.LittleEndian.PutUint32(wav[off+4:], uint32(len(wav)-off-8))
37+ }
38+ break
39+ }
40+ off += 8 + size + size%2
41+ }
42+ if int(binary.LittleEndian.Uint32(wav[4:8])) != len(wav)-8 {
43+ binary.LittleEndian.PutUint32(wav[4:], uint32(len(wav)-8))
44+ }
45+}
46+
47 // finishTurn writes a successful turn response from the display entry the
48 // orchestrator just recorded. The session lock must be held.
49 func finishTurn(w http.ResponseWriter, sess *session, res game.TurnResult) {