TTS: gracefully handle missing piper support

Third party redistributors might choose to skip distributing this for a
couple reasons:
- missing dependencies
- lack of interest in TTS as a feature

Lay some groundwork for handling this with fewer error message popups.
In particular note that speechd / flite depend on PyQt6 being built with
it, so support *may* appear dynamically after calibre is installed, and
available_engines queries Qt to see what is available. Piper is built
as part of calibre though, and if it has been patched out or skipped
via `setup.py build --only=xxx` we can at least avoid claiming it's
there.

Entrypoints into TTS eventually tend to consolidate into creating the
backend. This gives us one consistent place to raise errors for missing
backends... which however doesn't handle forcing a backend name. A
forced backend that is unavailable ended up hitting the "no prefs"
fallback code to use the default engine, which returned a different
backend than the one which is *forced*, and later a KeyError when
tweak_book attempted to access the backend name it forced but which
didn't exist.

Instead, raise an immediate "TTS engine piper is not available" error
dialog box, preventing any further confusing tracebacks.
This commit is contained in:
Eli Schwartz 2025-08-08 01:15:04 -04:00
parent de47227c2c
commit ee2e5374ce
No known key found for this signature in database
GPG Key ID: CEB167EFB5722BD6

View File

@ -234,11 +234,18 @@ def available_engines() -> dict[str, EngineMetadata]:
), True)
elif x == 'speechd':
continue
ans['piper'] = EngineMetadata('piper', _('The Piper Neural Engine'), _(
'The "piper" engine can track the currently spoken sentence on screen. It uses a neural network '
'for natural sounding voices. The neural network is run locally on your computer, it is fairly resource intensive to run.'
), TrackingCapability.Sentence, can_change_pitch=False, voices_have_quality_metadata=True, has_managed_voices=True,
has_sentence_delay=True)
try:
import calibre_extensions.piper
except ImportError:
pass
else:
ans['piper'] = EngineMetadata('piper', _('The Piper Neural Engine'), _(
'The "piper" engine can track the currently spoken sentence on screen. It uses a neural network '
'for natural sounding voices. The neural network is run locally on your computer, it is fairly resource intensive to run.'
), TrackingCapability.Sentence, can_change_pitch=False, voices_have_quality_metadata=True, has_managed_voices=True,
has_sentence_delay=True)
if islinux:
try:
from speechd.paths import SPD_SPAWN_CMD
@ -322,10 +329,15 @@ def create_tts_backend(force_engine: str | None = None, config_name: str = CONFI
if not available_engines():
raise OSError('There are no available TTS engines. Install a TTS engine before trying to use Read Aloud, such as flite or speech-dispatcher')
prefs = load_config(config_name)
engine_name = prefs.get('engine', '') if force_engine is None else force_engine
engine_name = engine_name or default_engine_name()
if engine_name not in available_engines():
engine_name = default_engine_name()
if force_engine is not None:
engine_name = force_engine
if engine_name not in available_engines():
raise OSError(f'TTS engine {force_engine} is not available.')
else:
engine_name = prefs.get('engine', '')
engine_name = engine_name or default_engine_name()
if engine_name not in available_engines():
engine_name = default_engine_name()
if engine_name == 'piper':
if engine_name not in engine_instances:
from calibre.gui2.tts.piper import Piper