Skip to content

TTS speak() blocks the main thread on every spoken selection, freezing the gaze cursor #679

Description

Why

VocableTextToSpeech.speak() performs blocking binder IPC into the TTS engine process on the caller's thread — setLanguage() (up to twice, with locale fallback) plus the per-call tts.voices enumeration that powers #632's live default-voice fallback — and every production call site invokes it from the main thread. The calls can take tens to hundreds of milliseconds, during which the Choreographer stops ticking: the gaze cursor visibly freezes and gaze input effectively pauses on every spoken selection. This is currently masked by GazeClickable's post-selection hold (the button is deliberately frozen green during the same window), which is how it went unnoticed — it was exposed during #678's smoothing work when that hold was temporarily disabled.

Scope

Move speak()'s blocking engine work off the main thread without changing spoken behavior.

Acceptance Criteria

  • speak() performs no blocking TTS engine calls (setLanguage, voices enumeration, speak) on the caller's thread
  • Engine work is serialized so concurrent speak() calls cannot interleave engine-state mutations (single dedicated thread, not a pool)
  • The stale-selection signal still clears the persisted voice name at all call sites (return value becomes a callback or equivalent)
  • Spoken behavior is unchanged: utterance ordering, QUEUE_FLUSH semantics, and isSpeakingFlow transitions
  • Verified on-device: the gaze cursor keeps moving through the selection-to-speech gap

Out of Scope

  • Whether the post-selection hold itself should remain coupled to TTS completion (a confirmed iOS/Android behavior difference — product question, not yet tracked)
  • Any debug flag for skipping the selection hold (not yet tracked)

Links

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

Status
Ready to select

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions