Skip to content

Wire up TTS voice selection dropdown in Narration popover#1660

Open
4rdii wants to merge 1 commit intojohnfactotum:gtk4from
4rdii:feat/tts-voice-dropdown
Open

Wire up TTS voice selection dropdown in Narration popover#1660
4rdii wants to merge 1 commit intojohnfactotum:gtk4from
4rdii:feat/tts-voice-dropdown

Conversation

@4rdii
Copy link
Copy Markdown

@4rdii 4rdii commented Apr 11, 2026

Summary

Completes the commented-out voice dropdown stub that has been sitting in src/ui/tts-box.ui for some time, by:

  1. Adding SSIPClient.setVoice(name) in src/speech.js (one-line method, mirrors the existing setRate / setPitch pattern).
  2. Uncommenting the GtkDropDown and its label in src/ui/tts-box.ui.
  3. Wiring selection, population, and notify-suppression in src/tts.js on FoliateTTSBox.
  4. Triggering a lazy loadVoices() call from src/navbar.js when the Narration popover is first shown.

Design notes

  • Lazy loading. The dropdown is populated on popover show, not in the widget constructor. This preserves the current behavior where Speech Dispatcher is only spawned when the user explicitly uses TTS — users who never click the Narration button never incur an SSIP init.
  • No new toolbar widgets. The dropdown sits inside the existing popover next to Speed and Pitch. The toolbar surface is unchanged.
  • Entries labeled <name> (<lang>) when LIST SYNTHESIS_VOICES reports a language, otherwise just <name>. Labels are derived from whatever the active output module returns, so the experience scales to whichever TTS backend the user has configured (espeak-ng, piper, mimic3, mary, etc.).
  • Resume semantics match the sliders. Changing voice mid-playback briefly stops SSIP, applies the new voice, and resumes from the same position — the same pattern used by #connectScale for Rate/Pitch.
  • Suppression flag. Setting the dropdown model fires a notify::selected signal that would otherwise trigger setVoice on the default voice the first time the popover opens, unnecessarily interrupting any already-running speech. A small #suppressVoiceNotify flag guards against that.

Why this complements existing work

The commented-out stub at src/ui/tts-box.ui suggests the dropdown was always intended but not finished. This PR finishes it with the minimum amount of new code (~40 lines) and does not introduce any new dependencies. It also removes a commented-out block, which the CONTRIBUTING.md explicitly calls out as something to avoid in PRs.

Test plan

  • Built locally against Foliate 3.1.1 (Ubuntu 24.04 system libraries).
  • Verified the dropdown is empty until the Narration popover is first opened — SSIP is not spawned at startup.
  • Verified the dropdown populates with voices reported by the active Speech Dispatcher module.
  • Verified selecting a different voice while playing pauses, applies the voice, and resumes from the same position.
  • Verified no spurious setVoice is fired on initial model population.
  • Verified that if listSynthesisVoices returns an empty list, the dropdown stays empty and no error dialog is shown.

Tested with four Piper voice models (en_US and en_GB, female and male) routed through a custom Speech Dispatcher generic module, as well as the default espeak-ng module.

🤖 Generated with Claude Code

The Narration popover's tts-box.ui template has carried a commented-out
Voice label and GtkDropDown for some time. This change uncomments the
stub, wires it to Speech Dispatcher via a new SSIPClient.setVoice()
method, and populates it lazily the first time the popover is shown.

Behavior:

- First time the user clicks the Narration (headphones) button, the
  dropdown is populated by querying `LIST SYNTHESIS_VOICES` from the
  currently-active Speech Dispatcher output module. If the user never
  clicks the button, SSIP is never initialized — Speech Dispatcher is
  not spawned at application startup as a side effect.
- Selecting a different voice issues `SET SELF SYNTHESIS_VOICE <name>`.
  If TTS is currently playing it is briefly paused and then resumed
  from the same position with the new voice, matching the existing
  behavior of the Speed and Pitch scales.
- Entries are labeled `<name> (<lang>)` when the module reports a
  language, otherwise just `<name>`.
- A suppression flag prevents the initial model-population from
  firing a spurious notify::selected that would trigger setVoice on
  the default voice and briefly pause any in-flight speech.

This complements the existing Speed and Pitch scales in the popover
and does not add any new toolbar widgets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant