Skip to content

say bypass races chat turns under continuous ASR — sentence_id arbitration #104

Description

@BrettKinny

Context

Found during the #44 LED bench sweep on 2026-05-23. After the PR #103 fix, the room_view → `face_recognized` chain fires correctly:

  • VLM identifies the user (`match=brett`)
  • `face_recognized` event broadcast on the bus
  • `FaceGreeter` consumes it and calls `xiaozhi.say(device_id, "Oh, it's Brett!")`
  • xiaozhi-server logs `say queued device=… text="Oh, it's Brett!"`

But no audio plays on the device.

Root cause

`custom-providers/xiaozhi-patches/http_server.py` line 613-616, in the `_dotty_say` handler:

```python

The consumer thread filters with

`message.sentence_id != self.conn.sentence_id` and

drops anything that doesn't match — so we stamp the

conn with our new id BEFORE putting messages on the

queue. This will pre-empt any in-flight TTS for this

conn, which is acceptable for server-pushed greetings

(they shouldn't race a chat turn in normal operation).

```

The "shouldn't race a chat turn in normal operation" assumption breaks when:

  • Background audio (TV / podcast / radio / other conversation) keeps the mic open
  • Whisper ASR transcribes each ambient sentence as a "user message"
  • Each new ASR turn assigns a new `conn.sentence_id` BEFORE our enqueued say messages get consumed
  • The consumer thread filters out our say frames (sentence_id mismatch) and our greeting is silently dropped

Reproduction (2026-05-23 walk-in)

xiaozhi-server log fragment around the bench-test window:

```
20:27:07 大模型收到用户消息: {"content": "When somebody focuses on an external enemy..."}
20:28:24 大模型收到用户消息: {"content": "which I think are really old fashioned social democrats..."}
… (background TV ASR transcripts every ~5-60 s)
20:33:02 say queued device=1c:db:d4:ba:58:94 sid=6b6b7464 text="Oh, it's Brett!" ← never plays
20:35:38 say queued device=1c:db:d4:ba:58:94 sid=1b44bd34 text="Oh, it's Brett!" ← never plays
```

Possible fixes

  • (a) Hold the say's sentence_id for the consumer to drain — don't let a subsequent chat turn overwrite `conn.sentence_id` until the say's LAST message has been processed (small flag on the conn?). Race-prone.
  • (b) Use a separate `server_push_sentence_id` field the consumer checks alongside the chat sentence_id, so server-pushed audio doesn't share state with the chat-turn filter. Cleaner.
  • (c) Bypass the chat-turn TTS queue entirely for server-pushed greetings — render audio frames directly via the TTS provider and stream them out the WS without touching `conn.sentence_id`. Most invasive; requires a separate audio frame path.

Adjacent issue: continuous background ASR feeding the LLM as fake user messages is a separate problem (filed alongside as the "ambient ASR garbage" issue) — fixing that would reduce how often this race actually fires.

Severity

P1 — silently drops user-visible named-greetings in any room with ambient audio. The LED path still works so the user sees Dotty "recognise" them (pixel 6 green), but Dotty stays silent, which reads as a regression even though the LED + bus + cache all worked correctly.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:xiaozhiUnraid xiaozhi-server container + custom providersbugSomething isn't workingstatus:activeReady to start, not blocked

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions