Experimental Braille support added.
This commit is contained in:
95
bookstorm.py
95
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,7 +328,12 @@ class BookReader:
|
||||
print(f"[Paragraph {self.currentParagraph + 1}/{chapter.get_total_paragraphs()}]")
|
||||
print(f"\n{paragraph}\n")
|
||||
|
||||
# Generate and play audio
|
||||
# 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)
|
||||
@@ -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,9 +2207,22 @@ class BookReader:
|
||||
# Post pygame event to handle in main loop
|
||||
pygame.event.post(pygame.event.Event(SPEECH_FINISHED_EVENT))
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
@@ -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():
|
||||
|
||||
@@ -6,6 +6,13 @@ mutagen>=1.45.0
|
||||
pypdf
|
||||
mpv
|
||||
|
||||
# Braille display support (optional)
|
||||
# Note: These are system packages, not pip packages
|
||||
# Install with: sudo pacman -S brltty liblouis python-brlapi python-louis (Arch)
|
||||
# or: sudo apt install brltty liblouis-bin python3-brlapi python3-louis (Debian/Ubuntu)
|
||||
|
||||
# Optional dependencies
|
||||
# piper-tts: Install separately with voice models to /usr/share/piper-voices/
|
||||
# ffmpeg: Install via system package manager for M4B/M4A support
|
||||
# brltty: System daemon for Braille display hardware
|
||||
# liblouis: Braille translation library
|
||||
|
||||
285
src/braille_menu.py
Normal file
285
src/braille_menu.py
Normal file
@@ -0,0 +1,285 @@
|
||||
"""
|
||||
Braille settings menu for BookStorm.
|
||||
|
||||
Provides a submenu for configuring Braille display settings including:
|
||||
- Enable/disable Braille output
|
||||
- Translation table selection (Grade 1, Grade 2, UEB)
|
||||
- Sync mode (sync with TTS or manual)
|
||||
- Status message display toggle
|
||||
"""
|
||||
|
||||
import pygame
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BrailleMenu:
|
||||
"""Interactive menu for Braille display settings."""
|
||||
|
||||
def __init__(self, screen, speechEngine, brailleOutput, configManager):
|
||||
"""
|
||||
Initialize Braille menu.
|
||||
|
||||
Args:
|
||||
screen: Pygame display surface
|
||||
speechEngine: Speech engine for menu feedback
|
||||
brailleOutput: BrailleOutput instance to configure
|
||||
configManager: ConfigManager for saving settings
|
||||
"""
|
||||
self.screen = screen
|
||||
self.speechEngine = speechEngine
|
||||
self.brailleOutput = brailleOutput
|
||||
self.configManager = configManager
|
||||
self.menuItems = []
|
||||
self.currentIndex = 0
|
||||
self.running = False
|
||||
|
||||
# Build menu items
|
||||
self._build_menu()
|
||||
|
||||
def _build_menu(self):
|
||||
"""Build menu items based on current settings."""
|
||||
self.menuItems = [
|
||||
{
|
||||
"label": f"Braille Output: {'Enabled' if self.brailleOutput.enabled else 'Disabled'}",
|
||||
"action": self._toggle_enabled
|
||||
},
|
||||
{
|
||||
"label": f"Translation Table: {self._get_table_display_name()}",
|
||||
"action": self._cycle_table
|
||||
},
|
||||
{
|
||||
"label": f"Sync with TTS: {'Yes' if self.brailleOutput.syncWithTts else 'No'}",
|
||||
"action": self._toggle_sync
|
||||
},
|
||||
{
|
||||
"label": f"Show Status Messages: {'Yes' if self.brailleOutput.showStatus else 'No'}",
|
||||
"action": self._toggle_status
|
||||
},
|
||||
{
|
||||
"label": f"Mute Voice (Braille Only): {'Yes' if self.brailleOutput.muteVoice else 'No'}",
|
||||
"action": self._toggle_mute_voice
|
||||
},
|
||||
{
|
||||
"label": "Back to Options",
|
||||
"action": self._exit_menu
|
||||
}
|
||||
]
|
||||
|
||||
def _get_table_display_name(self):
|
||||
"""Get display name for current translation table."""
|
||||
tableNames = {
|
||||
"grade1": "Grade 1",
|
||||
"grade2": "Grade 2",
|
||||
"ueb": "UEB"
|
||||
}
|
||||
return tableNames.get(self.brailleOutput.translationTable, "Unknown")
|
||||
|
||||
def _toggle_enabled(self):
|
||||
"""Toggle Braille output enabled/disabled."""
|
||||
# Note: Toggling requires re-initialization
|
||||
newState = not self.brailleOutput.enabled
|
||||
|
||||
if newState and not self.brailleOutput.brl:
|
||||
# Try to initialize
|
||||
self.brailleOutput.enabled = True
|
||||
self.brailleOutput._initialize_braille()
|
||||
|
||||
if not self.brailleOutput.brl:
|
||||
self.speechEngine.speak("Braille initialization failed. Check brltty daemon.")
|
||||
self.brailleOutput.enabled = False
|
||||
else:
|
||||
self.speechEngine.speak("Braille output enabled")
|
||||
elif not newState:
|
||||
# Disable
|
||||
self.brailleOutput.close()
|
||||
self.speechEngine.speak("Braille output disabled")
|
||||
|
||||
# Save to config - save ALL settings when enabling to ensure config is complete
|
||||
self.configManager.set("Braille", "enabled", str(self.brailleOutput.enabled))
|
||||
if self.brailleOutput.enabled:
|
||||
# Save all settings to ensure config is complete
|
||||
self.configManager.set("Braille", "translation_table", self.brailleOutput.translationTable)
|
||||
self.configManager.set("Braille", "sync_with_tts", str(self.brailleOutput.syncWithTts))
|
||||
self.configManager.set("Braille", "show_status", str(self.brailleOutput.showStatus))
|
||||
self.configManager.set("Braille", "mute_voice", str(self.brailleOutput.muteVoice))
|
||||
self.configManager.save_settings()
|
||||
|
||||
# Rebuild menu to update labels
|
||||
self._build_menu()
|
||||
|
||||
def _cycle_table(self):
|
||||
"""Cycle through translation tables."""
|
||||
tables = ["grade1", "grade2", "ueb"]
|
||||
currentIdx = tables.index(self.brailleOutput.translationTable)
|
||||
nextIdx = (currentIdx + 1) % len(tables)
|
||||
newTable = tables[nextIdx]
|
||||
|
||||
self.brailleOutput.set_table(newTable)
|
||||
self.speechEngine.speak(f"Translation table: {self._get_table_display_name()}")
|
||||
|
||||
# Save to config
|
||||
self.configManager.set("Braille", "translation_table", newTable)
|
||||
self.configManager.save_settings()
|
||||
|
||||
# Rebuild menu
|
||||
self._build_menu()
|
||||
|
||||
def _toggle_sync(self):
|
||||
"""Toggle sync with TTS mode."""
|
||||
newSync = not self.brailleOutput.syncWithTts
|
||||
self.brailleOutput.set_sync_mode(newSync)
|
||||
|
||||
self.speechEngine.speak(f"Sync with TTS: {'enabled' if newSync else 'disabled'}")
|
||||
|
||||
# Save to config
|
||||
self.configManager.set("Braille", "sync_with_tts", str(newSync))
|
||||
self.configManager.save_settings()
|
||||
|
||||
# Rebuild menu
|
||||
self._build_menu()
|
||||
|
||||
def _toggle_status(self):
|
||||
"""Toggle status message display."""
|
||||
newStatus = not self.brailleOutput.showStatus
|
||||
self.brailleOutput.set_show_status(newStatus)
|
||||
|
||||
self.speechEngine.speak(f"Status messages: {'enabled' if newStatus else 'disabled'}")
|
||||
|
||||
# Save to config
|
||||
self.configManager.set("Braille", "show_status", str(newStatus))
|
||||
self.configManager.save_settings()
|
||||
|
||||
# Rebuild menu
|
||||
self._build_menu()
|
||||
|
||||
def _toggle_mute_voice(self):
|
||||
"""Toggle mute voice (Braille-only mode)."""
|
||||
newMute = not self.brailleOutput.muteVoice
|
||||
self.brailleOutput.set_mute_voice(newMute)
|
||||
|
||||
self.speechEngine.speak(f"Braille-only mode: {'enabled' if newMute else 'disabled'}")
|
||||
|
||||
# Save to config
|
||||
self.configManager.set("Braille", "mute_voice", str(newMute))
|
||||
self.configManager.save_settings()
|
||||
|
||||
# Rebuild menu
|
||||
self._build_menu()
|
||||
|
||||
def _exit_menu(self):
|
||||
"""Exit the Braille settings menu."""
|
||||
self.running = False
|
||||
|
||||
def show(self):
|
||||
"""Display and run the Braille settings menu."""
|
||||
self.running = True
|
||||
self.currentIndex = 0
|
||||
|
||||
# Announce menu
|
||||
self.speechEngine.speak("Braille Settings")
|
||||
|
||||
# Show first item
|
||||
if self.menuItems:
|
||||
self._speak_current_item()
|
||||
|
||||
# Event loop
|
||||
clock = pygame.time.Clock()
|
||||
while self.running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
self.running = False
|
||||
return False # Signal app should quit
|
||||
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
self._exit_menu()
|
||||
|
||||
elif event.key == pygame.K_UP:
|
||||
self._navigate_up()
|
||||
|
||||
elif event.key == pygame.K_DOWN:
|
||||
self._navigate_down()
|
||||
|
||||
elif event.key == pygame.K_RETURN or event.key == pygame.K_SPACE:
|
||||
self._activate_current()
|
||||
|
||||
# Render menu
|
||||
self._render()
|
||||
clock.tick(30)
|
||||
|
||||
return True # Menu exited normally
|
||||
|
||||
def _navigate_up(self):
|
||||
"""Navigate to previous menu item."""
|
||||
if self.menuItems:
|
||||
self.currentIndex = (self.currentIndex - 1) % len(self.menuItems)
|
||||
self._speak_current_item()
|
||||
|
||||
def _navigate_down(self):
|
||||
"""Navigate to next menu item."""
|
||||
if self.menuItems:
|
||||
self.currentIndex = (self.currentIndex + 1) % len(self.menuItems)
|
||||
self._speak_current_item()
|
||||
|
||||
def _activate_current(self):
|
||||
"""Activate the current menu item."""
|
||||
if self.menuItems and 0 <= self.currentIndex < len(self.menuItems):
|
||||
item = self.menuItems[self.currentIndex]
|
||||
if "action" in item:
|
||||
item["action"]()
|
||||
|
||||
def _speak_current_item(self):
|
||||
"""Speak and display the current menu item."""
|
||||
if self.menuItems and 0 <= self.currentIndex < len(self.menuItems):
|
||||
item = self.menuItems[self.currentIndex]
|
||||
self.speechEngine.speak(item["label"])
|
||||
|
||||
# Show on Braille display
|
||||
if self.brailleOutput.enabled:
|
||||
self.brailleOutput.show_menu_item(
|
||||
item["label"],
|
||||
self.currentIndex + 1,
|
||||
len(self.menuItems)
|
||||
)
|
||||
|
||||
def _render(self):
|
||||
"""Render the menu on screen."""
|
||||
# Clear screen
|
||||
self.screen.fill((0, 0, 0))
|
||||
|
||||
# Get screen dimensions
|
||||
width, height = self.screen.get_size()
|
||||
|
||||
# Font setup
|
||||
try:
|
||||
menuFont = pygame.font.Font(None, 36)
|
||||
titleFont = pygame.font.Font(None, 48)
|
||||
except:
|
||||
menuFont = pygame.font.SysFont('dejavu', 36)
|
||||
titleFont = pygame.font.SysFont('dejavu', 48)
|
||||
|
||||
# Draw title
|
||||
titleSurface = titleFont.render("Braille Settings", True, (255, 255, 255))
|
||||
titleRect = titleSurface.get_rect(center=(width // 2, 50))
|
||||
self.screen.blit(titleSurface, titleRect)
|
||||
|
||||
# Draw menu items
|
||||
yOffset = 150
|
||||
for i, item in enumerate(self.menuItems):
|
||||
color = (255, 255, 0) if i == self.currentIndex else (255, 255, 255)
|
||||
itemSurface = menuFont.render(item["label"], True, color)
|
||||
itemRect = itemSurface.get_rect(center=(width // 2, yOffset))
|
||||
self.screen.blit(itemSurface, itemRect)
|
||||
yOffset += 80
|
||||
|
||||
# Draw instructions
|
||||
instructionsFont = pygame.font.Font(None, 24)
|
||||
instructions = "UP/DOWN: Navigate | ENTER: Select | ESC: Back"
|
||||
instrSurface = instructionsFont.render(instructions, True, (128, 128, 128))
|
||||
instrRect = instrSurface.get_rect(center=(width // 2, height - 30))
|
||||
self.screen.blit(instrSurface, instrRect)
|
||||
|
||||
pygame.display.flip()
|
||||
352
src/braille_output.py
Normal file
352
src/braille_output.py
Normal file
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
Braille display output using brltty and liblouis.
|
||||
|
||||
This module provides Braille display support for BookStorm, allowing users
|
||||
to read book content on refreshable Braille displays. It uses:
|
||||
- brltty (via BrlAPI): Hardware communication layer
|
||||
- liblouis: Text-to-Braille translation
|
||||
|
||||
Supports Grade 1, Grade 2, and UEB Braille tables.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BrailleOutput:
|
||||
"""Manages Braille display output via brltty and liblouis."""
|
||||
|
||||
# Translation table mappings
|
||||
TABLES = {
|
||||
"grade1": "en-us-g1.ctb",
|
||||
"grade2": "en-us-g2.ctb",
|
||||
"ueb": "en-ueb-g2.ctb"
|
||||
}
|
||||
|
||||
def __init__(self, enabled=False, translationTable="grade2", syncWithTts=True, showStatus=True, muteVoice=False):
|
||||
"""
|
||||
Initialize Braille output.
|
||||
|
||||
Args:
|
||||
enabled: Whether Braille output is enabled
|
||||
translationTable: Translation table name (grade1/grade2/ueb)
|
||||
syncWithTts: Whether to sync Braille with TTS playback
|
||||
showStatus: Whether to show status messages on Braille display
|
||||
muteVoice: Whether to mute TTS when Braille is active (Braille-only mode)
|
||||
"""
|
||||
self.enabled = enabled
|
||||
self.translationTable = translationTable
|
||||
self.syncWithTts = syncWithTts
|
||||
self.showStatus = showStatus
|
||||
self.muteVoice = muteVoice
|
||||
self.brl = None
|
||||
self.displaySize = (0, 0)
|
||||
self.currentText = ""
|
||||
self.panOffset = 0
|
||||
|
||||
# Import libraries only if enabled
|
||||
if self.enabled:
|
||||
self._initialize_braille()
|
||||
|
||||
def _initialize_braille(self):
|
||||
"""Initialize brltty connection and liblouis."""
|
||||
try:
|
||||
import brlapi
|
||||
import louis
|
||||
self.brlapi = brlapi
|
||||
self.louis = louis
|
||||
|
||||
print("DEBUG: Attempting to connect to brltty...")
|
||||
logger.info("Attempting to connect to brltty daemon")
|
||||
|
||||
# Connect to brltty daemon
|
||||
self.brl = brlapi.Connection()
|
||||
self.displaySize = self.brl.displaySize
|
||||
|
||||
print(f"DEBUG: Braille display connected! Size: {self.displaySize[0]} x {self.displaySize[1]} cells")
|
||||
print(f"DEBUG: Driver: {self.brl.driverName}")
|
||||
logger.info(f"Braille display connected: {self.displaySize[0]} x {self.displaySize[1]} cells")
|
||||
|
||||
# Enter TTY mode - this takes exclusive control from screen reader
|
||||
print("DEBUG: Entering TTY mode (taking control from screen reader)...")
|
||||
self.brl.enterTtyMode()
|
||||
print("DEBUG: TTY mode entered successfully")
|
||||
logger.info("Entered TTY mode - have exclusive control of Braille display")
|
||||
|
||||
# Verify translation table exists
|
||||
tablePath = self.TABLES.get(self.translationTable, self.TABLES["grade2"])
|
||||
try:
|
||||
# Test translation
|
||||
testResult = self.louis.translateString([tablePath], "test")
|
||||
print(f"DEBUG: Braille translation working. Table: {tablePath}")
|
||||
logger.info(f"Using Braille table: {tablePath}")
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Translation table error: {e}, falling back to grade2")
|
||||
logger.warning(f"Translation table error: {e}, falling back to grade2")
|
||||
self.translationTable = "grade2"
|
||||
|
||||
except ImportError as e:
|
||||
print(f"DEBUG: ImportError - BrlAPI or liblouis not installed: {e}")
|
||||
logger.error("BrlAPI or liblouis not installed. Install python3-brlapi and python3-louis")
|
||||
self.enabled = False
|
||||
self.brl = None
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Could not connect to brltty: {e}")
|
||||
print("DEBUG: Is brltty daemon running? Check with: systemctl status brltty")
|
||||
logger.warning(f"Could not connect to brltty: {e}")
|
||||
self.enabled = False
|
||||
self.brl = None
|
||||
|
||||
def show_text(self, text, cursorPos=0):
|
||||
"""
|
||||
Display text on Braille display.
|
||||
|
||||
Args:
|
||||
text: Text to display (will be translated to Braille)
|
||||
cursorPos: Cursor position (0-based)
|
||||
"""
|
||||
if not self.enabled or not self.brl:
|
||||
return
|
||||
|
||||
try:
|
||||
# Store current text for panning
|
||||
self.currentText = text
|
||||
self.panOffset = 0
|
||||
|
||||
# Translate to Braille
|
||||
tablePath = self.TABLES.get(self.translationTable, self.TABLES["grade2"])
|
||||
brailleText = self.louis.translateString([tablePath], text)
|
||||
|
||||
# Handle text longer than display width
|
||||
displayWidth = self.displaySize[0]
|
||||
if len(brailleText) > displayWidth:
|
||||
# Show first portion
|
||||
brailleText = brailleText[:displayWidth]
|
||||
|
||||
# Send to display
|
||||
self.brl.writeText(brailleText, cursorPos)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error displaying Braille text: {e}")
|
||||
|
||||
def show_paragraph(self, paragraph, offset=0):
|
||||
"""
|
||||
Display a paragraph with panning support.
|
||||
|
||||
Args:
|
||||
paragraph: Full paragraph text
|
||||
offset: Character offset for panning
|
||||
"""
|
||||
if not self.enabled:
|
||||
print("DEBUG: Braille output disabled, skipping")
|
||||
return
|
||||
|
||||
if not self.brl:
|
||||
print("DEBUG: No Braille connection, skipping")
|
||||
return
|
||||
|
||||
try:
|
||||
# Store for manual panning
|
||||
self.currentText = paragraph
|
||||
self.panOffset = offset
|
||||
|
||||
print(f"DEBUG: Translating paragraph (length {len(paragraph)}) to Braille...")
|
||||
|
||||
# Translate entire paragraph
|
||||
tablePath = self.TABLES.get(self.translationTable, self.TABLES["grade2"])
|
||||
brailleText = self.louis.translateString([tablePath], paragraph)
|
||||
|
||||
print(f"DEBUG: Braille text length: {len(brailleText)}, Display width: {self.displaySize[0]}")
|
||||
|
||||
# Extract window based on offset
|
||||
displayWidth = self.displaySize[0]
|
||||
window = brailleText[offset:offset + displayWidth]
|
||||
|
||||
# Pad if necessary
|
||||
if len(window) < displayWidth:
|
||||
window = window + " " * (displayWidth - len(window))
|
||||
|
||||
print(f"DEBUG: Sending {len(window)} cells to Braille display...")
|
||||
self.brl.writeText(window)
|
||||
print("DEBUG: Braille text sent successfully")
|
||||
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Error displaying paragraph: {e}")
|
||||
logger.error(f"Error displaying paragraph: {e}")
|
||||
|
||||
def show_status(self, message):
|
||||
"""
|
||||
Display a status message on Braille display.
|
||||
|
||||
Args:
|
||||
message: Status message to display
|
||||
"""
|
||||
if not self.enabled or not self.brl or not self.showStatus:
|
||||
return
|
||||
|
||||
try:
|
||||
# Translate and display
|
||||
tablePath = self.TABLES.get(self.translationTable, self.TABLES["grade2"])
|
||||
brailleText = self.louis.translateString([tablePath], message)
|
||||
|
||||
# Truncate if too long
|
||||
displayWidth = self.displaySize[0]
|
||||
if len(brailleText) > displayWidth:
|
||||
brailleText = brailleText[:displayWidth]
|
||||
|
||||
self.brl.writeText(brailleText)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error displaying status: {e}")
|
||||
|
||||
def show_menu_item(self, itemText, position=None, total=None):
|
||||
"""
|
||||
Display a menu item on Braille display.
|
||||
|
||||
Args:
|
||||
itemText: Menu item text
|
||||
position: Current position (1-based), optional
|
||||
total: Total items, optional
|
||||
"""
|
||||
if not self.enabled or not self.brl:
|
||||
return
|
||||
|
||||
try:
|
||||
# Format with position if provided
|
||||
if position is not None and total is not None:
|
||||
displayText = f"{position}/{total}: {itemText}"
|
||||
else:
|
||||
displayText = itemText
|
||||
|
||||
self.show_text(displayText)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error displaying menu item: {e}")
|
||||
|
||||
def pan_forward(self):
|
||||
"""Pan Braille display forward (right)."""
|
||||
if not self.enabled or not self.brl or not self.currentText:
|
||||
return
|
||||
|
||||
# Translate full text
|
||||
tablePath = self.TABLES.get(self.translationTable, self.TABLES["grade2"])
|
||||
brailleText = self.louis.translateString([tablePath], self.currentText)
|
||||
|
||||
displayWidth = self.displaySize[0]
|
||||
maxOffset = max(0, len(brailleText) - displayWidth)
|
||||
|
||||
# Advance by display width
|
||||
self.panOffset = min(self.panOffset + displayWidth, maxOffset)
|
||||
|
||||
# Show new window
|
||||
self.show_paragraph(self.currentText, self.panOffset)
|
||||
|
||||
def pan_backward(self):
|
||||
"""Pan Braille display backward (left)."""
|
||||
if not self.enabled or not self.brl or not self.currentText:
|
||||
return
|
||||
|
||||
displayWidth = self.displaySize[0]
|
||||
|
||||
# Go back by display width
|
||||
self.panOffset = max(0, self.panOffset - displayWidth)
|
||||
|
||||
# Show new window
|
||||
self.show_paragraph(self.currentText, self.panOffset)
|
||||
|
||||
def read_key(self, timeout=0):
|
||||
"""
|
||||
Read a key press from the Braille display.
|
||||
|
||||
Args:
|
||||
timeout: Timeout in milliseconds (0 = non-blocking)
|
||||
|
||||
Returns:
|
||||
Key code or None if no key pressed
|
||||
"""
|
||||
if not self.enabled or not self.brl:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Read key with timeout (non-blocking if timeout=0)
|
||||
keyCode = self.brl.readKey(wait=False)
|
||||
return keyCode
|
||||
except Exception as e:
|
||||
# No key pressed or error
|
||||
return None
|
||||
|
||||
def clear(self):
|
||||
"""Clear the Braille display."""
|
||||
if not self.enabled or not self.brl:
|
||||
return
|
||||
|
||||
try:
|
||||
self.brl.writeText("")
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing display: {e}")
|
||||
|
||||
def set_table(self, tableName):
|
||||
"""
|
||||
Change the Braille translation table.
|
||||
|
||||
Args:
|
||||
tableName: Table name (grade1/grade2/ueb)
|
||||
"""
|
||||
if tableName in self.TABLES:
|
||||
self.translationTable = tableName
|
||||
logger.info(f"Switched to Braille table: {tableName}")
|
||||
else:
|
||||
logger.warning(f"Unknown table: {tableName}, keeping {self.translationTable}")
|
||||
|
||||
def set_sync_mode(self, syncWithTts):
|
||||
"""
|
||||
Set sync mode.
|
||||
|
||||
Args:
|
||||
syncWithTts: True to sync with TTS, False for manual only
|
||||
"""
|
||||
self.syncWithTts = syncWithTts
|
||||
logger.info(f"Braille sync mode: {'TTS sync' if syncWithTts else 'manual'}")
|
||||
|
||||
def set_show_status(self, showStatus):
|
||||
"""
|
||||
Set whether to show status messages.
|
||||
|
||||
Args:
|
||||
showStatus: True to show status messages
|
||||
"""
|
||||
self.showStatus = showStatus
|
||||
logger.info(f"Braille status messages: {'enabled' if showStatus else 'disabled'}")
|
||||
|
||||
def set_mute_voice(self, muteVoice):
|
||||
"""
|
||||
Set whether to mute TTS when Braille is active.
|
||||
|
||||
Args:
|
||||
muteVoice: True to mute TTS (Braille-only mode)
|
||||
"""
|
||||
self.muteVoice = muteVoice
|
||||
logger.info(f"Braille-only mode: {'enabled' if muteVoice else 'disabled (dual output)'}")
|
||||
|
||||
def close(self):
|
||||
"""Close brltty connection and release display."""
|
||||
if self.brl:
|
||||
try:
|
||||
print("DEBUG: Leaving TTY mode and closing Braille connection...")
|
||||
# Leave TTY mode first to release control back to screen reader
|
||||
self.brl.leaveTtyMode()
|
||||
self.brl.closeConnection()
|
||||
print("DEBUG: Braille display connection closed")
|
||||
logger.info("Braille display connection closed")
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Error closing Braille connection: {e}")
|
||||
logger.error(f"Error closing Braille connection: {e}")
|
||||
finally:
|
||||
self.brl = None
|
||||
self.enabled = False
|
||||
|
||||
def __del__(self):
|
||||
"""Ensure connection is closed on cleanup."""
|
||||
self.close()
|
||||
@@ -76,6 +76,14 @@ class ConfigManager:
|
||||
'stream_cache_limit': '500'
|
||||
}
|
||||
|
||||
self.config['Braille'] = {
|
||||
'enabled': 'false',
|
||||
'translation_table': 'grade2',
|
||||
'sync_with_tts': 'true',
|
||||
'show_status': 'true',
|
||||
'mute_voice': 'false'
|
||||
}
|
||||
|
||||
self.save()
|
||||
|
||||
def get(self, section, key, fallback=None):
|
||||
@@ -326,3 +334,55 @@ class ConfigManager:
|
||||
speed = max(0.5, min(2.0, float(speed)))
|
||||
self.set('Audio', 'playback_speed', str(speed))
|
||||
self.save()
|
||||
|
||||
# Braille settings
|
||||
|
||||
def get_braille_enabled(self):
|
||||
"""Get Braille output enabled setting"""
|
||||
return self.get_bool('Braille', 'enabled', False)
|
||||
|
||||
def set_braille_enabled(self, enabled):
|
||||
"""Set Braille output enabled setting"""
|
||||
self.set('Braille', 'enabled', str(enabled).lower())
|
||||
self.save()
|
||||
|
||||
def get_braille_translation_table(self):
|
||||
"""Get Braille translation table (grade1/grade2/ueb)"""
|
||||
return self.get('Braille', 'translation_table', 'grade2')
|
||||
|
||||
def set_braille_translation_table(self, table):
|
||||
"""Set Braille translation table"""
|
||||
if table in ['grade1', 'grade2', 'ueb']:
|
||||
self.set('Braille', 'translation_table', table)
|
||||
self.save()
|
||||
|
||||
def get_braille_sync_with_tts(self):
|
||||
"""Get Braille sync with TTS setting"""
|
||||
return self.get_bool('Braille', 'sync_with_tts', True)
|
||||
|
||||
def set_braille_sync_with_tts(self, enabled):
|
||||
"""Set Braille sync with TTS setting"""
|
||||
self.set('Braille', 'sync_with_tts', str(enabled).lower())
|
||||
self.save()
|
||||
|
||||
def get_braille_show_status(self):
|
||||
"""Get Braille show status messages setting"""
|
||||
return self.get_bool('Braille', 'show_status', True)
|
||||
|
||||
def set_braille_show_status(self, enabled):
|
||||
"""Set Braille show status messages setting"""
|
||||
self.set('Braille', 'show_status', str(enabled).lower())
|
||||
self.save()
|
||||
|
||||
def get_braille_mute_voice(self):
|
||||
"""Get Braille mute voice (Braille-only mode) setting"""
|
||||
return self.get_bool('Braille', 'mute_voice', False)
|
||||
|
||||
def set_braille_mute_voice(self, enabled):
|
||||
"""Set Braille mute voice setting"""
|
||||
self.set('Braille', 'mute_voice', str(enabled).lower())
|
||||
self.save()
|
||||
|
||||
def save_settings(self):
|
||||
"""Alias for save() - for backward compatibility"""
|
||||
self.save()
|
||||
|
||||
@@ -14,7 +14,7 @@ from src.tts_engine import TtsEngine
|
||||
class OptionsMenu:
|
||||
"""Options menu for configuring BookStorm settings"""
|
||||
|
||||
def __init__(self, config, speechEngine, voiceSelector, audioPlayer, ttsReloadCallback=None):
|
||||
def __init__(self, config, speechEngine, voiceSelector, audioPlayer, ttsReloadCallback=None, brailleOutput=None):
|
||||
"""
|
||||
Initialize options menu
|
||||
|
||||
@@ -24,12 +24,14 @@ class OptionsMenu:
|
||||
voiceSelector: VoiceSelector instance
|
||||
audioPlayer: MpvPlayer instance
|
||||
ttsReloadCallback: Optional callback to reload TTS engine
|
||||
brailleOutput: Optional BrailleOutput instance
|
||||
"""
|
||||
self.config = config
|
||||
self.speechEngine = speechEngine
|
||||
self.voiceSelector = voiceSelector
|
||||
self.audioPlayer = audioPlayer
|
||||
self.ttsReloadCallback = ttsReloadCallback
|
||||
self.brailleOutput = brailleOutput
|
||||
self.currentSelection = 0
|
||||
self.inMenu = False
|
||||
|
||||
@@ -78,6 +80,10 @@ class OptionsMenu:
|
||||
'label': "Speech Rate Settings",
|
||||
'action': 'speech_rate'
|
||||
},
|
||||
{
|
||||
'label': "Braille Settings",
|
||||
'action': 'braille_settings'
|
||||
},
|
||||
{
|
||||
'label': absLabel,
|
||||
'action': 'audiobookshelf_setup'
|
||||
@@ -132,6 +138,8 @@ class OptionsMenu:
|
||||
return self._toggle_show_text()
|
||||
elif action == 'speech_rate':
|
||||
return self._speech_rate_info()
|
||||
elif action == 'braille_settings':
|
||||
return self._braille_settings()
|
||||
elif action == 'audiobookshelf_setup':
|
||||
return self._audiobookshelf_setup()
|
||||
elif action == 'back':
|
||||
@@ -493,6 +501,40 @@ class OptionsMenu:
|
||||
if menuItems and self.currentSelection < len(menuItems):
|
||||
self.speechEngine.speak(menuItems[self.currentSelection]['label'])
|
||||
|
||||
def _braille_settings(self):
|
||||
"""Open Braille settings submenu"""
|
||||
if not self.brailleOutput:
|
||||
self.speechEngine.speak("Braille output not initialized")
|
||||
return True
|
||||
|
||||
# Import here to avoid circular dependency
|
||||
from src.braille_menu import BrailleMenu
|
||||
import pygame
|
||||
|
||||
# Create Braille menu
|
||||
# Get pygame screen if available
|
||||
screen = pygame.display.get_surface()
|
||||
if not screen:
|
||||
# Create minimal display for menu
|
||||
screen = pygame.display.set_mode((1600, 900))
|
||||
|
||||
brailleMenu = BrailleMenu(screen, self.speechEngine, self.brailleOutput, self.config)
|
||||
|
||||
# Show Braille menu (blocks until user exits)
|
||||
continueRunning = brailleMenu.show()
|
||||
|
||||
# Return to options menu
|
||||
if continueRunning:
|
||||
self.speechEngine.speak("Back to options menu")
|
||||
# Speak current menu item
|
||||
menuItems = self.show_main_menu()
|
||||
if menuItems and self.currentSelection < len(menuItems):
|
||||
self.speechEngine.speak(menuItems[self.currentSelection]['label'])
|
||||
return True
|
||||
else:
|
||||
# User closed the application
|
||||
return False
|
||||
|
||||
def _audiobookshelf_setup(self):
|
||||
"""Setup Audiobookshelf server connection"""
|
||||
from src.ui import get_input
|
||||
|
||||
Reference in New Issue
Block a user