Skip to content

sherpa-onnx.rn/android: stopTts() cannot interrupt an in-flight TTS generation #440

Description

@abretonc7s

Summary

On Android, stopTts() is a no-op while TTS generation is running. It only takes effect after generation has already finished on its own.

Cause

TtsHandler runs init, generate, stop, and release on a single shared executor:

packages/sherpa-onnx.rn/android/src/main/kotlin/net/siteed/sherpaonnx/handlers/TtsHandler.kt:20

private val executor = Executors.newSingleThreadExecutor()

Call sites at :41 (init), :396 (generate), :745 (stop), :780 (release).

stop() therefore queues behind the in-flight generate() and cannot clear isGenerating while the generation callback loop is reading it. The isGenerating checks inside the callback (:463) and at the short-utterance fallback (:553) can never fire during the generation they are meant to interrupt.

Reproduction

Confirmed on a Pixel 6a via __AGENTIC__.testTTSStopDuringPrefill(120) in apps/sherpa-voice: calling stopTts() 120 ms into generation produced no Stopping TTS generation log line until after generation completed. Logcat showed generation and prefill running to completion uninterrupted.

Current state

isGenerating was marked @Volatile in #434 so the cross-thread write is at least visible. That is a correct partial step but changes no observable behaviorstopTts() during generation is still a no-op. Filing this so the volatile annotation is not later mistaken for evidence the problem was solved.

The limitation is documented in three places (TtsHandler.kt:22-23, TtsHandler.kt:544-551, and the testTTSStopDuringPrefill hook in apps/sherpa-voice/src/agentic-bridge.ts), and testTTSStopDuringPrefill is retained as a regression probe for when this is fixed.

Suggested fix

Run stop() on a separate control executor (or directly, with appropriate synchronization) so it can flip isGenerating while generation is in flight. Note that stop() currently only pauses a track already in PLAYSTATE_PLAYING; a track still being prefilled is STOPPED, so the pause/flush would be skipped. #434 already guards the post-loop playback start on isGenerating in anticipation of exactly that.

Also worth deciding whether iOS has the same behavior, or whether the platforms should be aligned.

Found during cross-review of #434.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions