Experimental Braille support added.
This commit is contained in:
197
bookstorm.py
197
bookstorm.py
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user