Experimental Braille support added.

This commit is contained in:
Storm Dragon
2025-10-19 18:02:34 -04:00
parent 16e01cb1f5
commit f9564265fa
6 changed files with 891 additions and 54 deletions

View File

@@ -51,6 +51,8 @@ from src.audiobookshelf_menu import AudiobookshelfMenu
from src.server_link_manager import ServerLinkManager
from src.bookmarks_menu import BookmarksMenu
from src.wav_exporter import WavExporter
from src.braille_output import BrailleOutput
from src.braille_menu import BrailleMenu
class BookReader:
@@ -80,6 +82,20 @@ class BookReader:
speechRate = self.config.get_speech_rate()
self.speechEngine.set_rate(speechRate)
# Initialize Braille output (before options menu)
brailleEnabled = self.config.get_braille_enabled()
brailleTable = self.config.get_braille_translation_table()
brailleSyncTts = self.config.get_braille_sync_with_tts()
brailleShowStatus = self.config.get_braille_show_status()
brailleMuteVoice = self.config.get_braille_mute_voice()
self.brailleOutput = BrailleOutput(
enabled=brailleEnabled,
translationTable=brailleTable,
syncWithTts=brailleSyncTts,
showStatus=brailleShowStatus,
muteVoice=brailleMuteVoice
)
# Initialize options menu
voiceSelector = VoiceSelector(self.config.get_voice_dir())
# Create callback reference for TTS engine reloading
@@ -89,7 +105,8 @@ class BookReader:
self.speechEngine,
voiceSelector,
self.audioPlayer,
ttsReloadCallback=reloadCallback
ttsReloadCallback=reloadCallback,
brailleOutput=self.brailleOutput
)
# Initialize book selector
@@ -311,17 +328,22 @@ class BookReader:
print(f"[Paragraph {self.currentParagraph + 1}/{chapter.get_total_paragraphs()}]")
print(f"\n{paragraph}\n")
# Generate and play audio
try:
print("Generating speech...")
wavData = self.ttsEngine.text_to_wav_data(paragraph)
if wavData:
print("Playing...")
completed = self.audioPlayer.play_wav_data(wavData, blocking=True)
return completed
except Exception as e:
print(f"Error during playback: {e}")
return False
# Show on Braille display
if self.brailleOutput.enabled:
self.brailleOutput.show_paragraph(paragraph)
# Generate and play audio (unless muted for Braille-only mode)
if not self.brailleOutput.muteVoice:
try:
print("Generating speech...")
wavData = self.ttsEngine.text_to_wav_data(paragraph)
if wavData:
print("Playing...")
completed = self.audioPlayer.play_wav_data(wavData, blocking=True)
return completed
except Exception as e:
print(f"Error during playback: {e}")
return False
return True
@@ -684,6 +706,12 @@ class BookReader:
# Start next paragraph
self._start_paragraph_playback()
# Check for Braille display key presses (panning, routing, etc.)
if self.brailleOutput.enabled:
brailleKey = self.brailleOutput.read_key()
if brailleKey:
self._handle_braille_key(brailleKey)
# Explicitly delete event objects to help GC
del events
@@ -830,6 +858,53 @@ class BookReader:
self.cachedSurfaces.clear()
pygame.quit()
def _handle_braille_key(self, keyCode):
"""
Handle key presses from Braille display.
Args:
keyCode: Key code from brltty
"""
# BrlAPI key codes (common ones)
# Forward panning: typically CMD_FWINRT or specific device codes
# Backward panning: typically CMD_FWINLT
# The exact codes depend on the device, so we check the command type
try:
# Convert key code to command
# BrlAPI provides key codes as integers
# Common commands (from brltty documentation):
# FWINRT (forward) and FWINLT (backward) for panning
# For debugging, print the key code
print(f"DEBUG: Braille key pressed: {keyCode}")
# Check if it's a panning command
# The key format varies by device, but we can check ranges
# Typically: 0x04000000 range for panning commands
# Try to parse as BrlAPI command
command = keyCode & 0x00FFFFFF # Extract command part
# Common commands (approximate values, device-specific)
CMD_FWINRT = 0x0001 # Pan forward/right
CMD_FWINLT = 0x0002 # Pan backward/left
if command == CMD_FWINRT or (keyCode & 0xFF) == 0x01:
# Forward panning
print("DEBUG: Braille pan forward")
self.brailleOutput.pan_forward()
elif command == CMD_FWINLT or (keyCode & 0xFF) == 0x02:
# Backward panning
print("DEBUG: Braille pan backward")
self.brailleOutput.pan_backward()
else:
# Unknown key - just log it for now
print(f"DEBUG: Unknown Braille key: 0x{keyCode:08x}")
except Exception as e:
print(f"DEBUG: Error handling Braille key: {e}")
def _handle_pygame_key(self, event):
"""Handle pygame key event"""
# Check if in Audiobookshelf menu
@@ -2132,50 +2207,63 @@ class BookReader:
# Post pygame event to handle in main loop
pygame.event.post(pygame.event.Event(SPEECH_FINISHED_EVENT))
self.readingEngine.speak_reading(paragraph, callback=on_speech_finished)
# Show on Braille display (unless muted for Braille-only mode)
if self.brailleOutput.enabled:
self.brailleOutput.show_paragraph(paragraph)
# Speak (unless muted for Braille-only mode)
if not self.brailleOutput.muteVoice:
self.readingEngine.speak_reading(paragraph, callback=on_speech_finished)
else:
# Use piper-tts for reading with buffering
wavData = None
try:
# Check if we have buffered audio ready
with self.bufferLock:
if self.bufferedAudio is not None:
# Use pre-generated audio
wavData = self.bufferedAudio
self.bufferedAudio = None
# Show on Braille display
if self.brailleOutput.enabled:
self.brailleOutput.show_paragraph(paragraph)
# Only generate/play audio if voice not muted
if not self.brailleOutput.muteVoice:
wavData = None
try:
# Check if we have buffered audio ready
with self.bufferLock:
if self.bufferedAudio is not None:
# Use pre-generated audio
wavData = self.bufferedAudio
self.bufferedAudio = None
else:
# Generate audio now (first paragraph or after navigation)
wavData = self.ttsEngine.text_to_wav_data(paragraph)
if wavData:
# Stop any existing audio playback
if self.audioPlayer.is_audio_file_playing():
self.audioPlayer.stop_audio_file()
# Get current playback speed from config
playbackSpeed = self.config.get_playback_speed()
# Play WAV data through MpvPlayer (which supports pause/resume)
if self.audioPlayer.play_wav_data(wavData, playbackSpeed=playbackSpeed):
# Start buffering next paragraph in background
self._buffer_next_paragraph()
else:
print("Error: Failed to start TTS playback")
self.isPlaying = False
# Explicitly delete wavData after playback starts to free memory
del wavData
wavData = None
else:
# Generate audio now (first paragraph or after navigation)
wavData = self.ttsEngine.text_to_wav_data(paragraph)
if wavData:
# Stop any existing audio playback
if self.audioPlayer.is_audio_file_playing():
self.audioPlayer.stop_audio_file()
# Get current playback speed from config
playbackSpeed = self.config.get_playback_speed()
# Play WAV data through MpvPlayer (which supports pause/resume)
if self.audioPlayer.play_wav_data(wavData, playbackSpeed=playbackSpeed):
# Start buffering next paragraph in background
self._buffer_next_paragraph()
else:
print("Error: Failed to start TTS playback")
self.isPlaying = False
# Explicitly delete wavData after playback starts to free memory
del wavData
wavData = None
else:
print("Warning: No audio data generated")
except Exception as e:
print(f"Error during playback: {e}")
# Stop playback on error to prevent infinite error loop
self.isPlaying = False
raise
finally:
# The variable is already set to None in all relevant paths
pass
print("Warning: No audio data generated")
except Exception as e:
print(f"Error during playback: {e}")
# Stop playback on error to prevent infinite error loop
self.isPlaying = False
raise
finally:
# The variable is already set to None in all relevant paths
pass
def _start_audio_chapter_playback(self, chapter):
"""Start playing audio book chapter"""
@@ -2365,6 +2453,9 @@ class BookReader:
self.readingEngine.cleanup()
if self.parser:
self.parser.cleanup()
# Cleanup Braille display
if self.brailleOutput:
self.brailleOutput.close()
def main():