Fixed pause which broke in the switch to mpv. Fixed bookmarks being lost when switching books using the recent bookmarks.

This commit is contained in:
Storm Dragon
2025-10-19 02:32:55 -04:00
parent 0bdc5bdf17
commit a934c06f6f
2 changed files with 100 additions and 134 deletions

View File

@@ -73,8 +73,7 @@ class BookReader:
self.parser = None # Will be set based on file type
self.bookmarkManager = BookmarkManager()
self.speechEngine = SpeechEngine() # UI feedback
self.audioPlayer = MpvPlayer()
self.ttsMpvProcess = None # For direct mpv subprocess for TTS
self.audioPlayer = MpvPlayer() # Used for both audio books and TTS playback
# Configure speech engine from saved settings
speechRate = self.config.get_speech_rate()
@@ -736,31 +735,25 @@ class BookReader:
# Start next chapter
self._start_paragraph_playback()
elif readerEngine == 'piper':
# Check piper-tts subprocess state
# The TTS mpv process runs independently, so we need to check if it's still running
playbackFinished = False
if self.ttsMpvProcess:
# Check if the mpv subprocess has finished
if self.ttsMpvProcess.poll() is not None:
playbackFinished = True
else:
# No process exists, consider playback finished
playbackFinished = True
# Check if TTS audio has finished playing
# Only auto-advance if NOT paused (to avoid skipping when user pauses)
if not self.audioPlayer.is_paused():
playbackFinished = not self.audioPlayer.is_audio_file_playing()
if playbackFinished:
# Current paragraph finished, advance
if not self.next_paragraph():
self.displayText = "End of book reached"
self.isPlaying = False
self.save_bookmark(speakFeedback=False)
else:
# Start next paragraph with error recovery
try:
self._start_paragraph_playback()
except Exception as e:
print(f"Error starting playback: {e}")
self.speechEngine.speak("Playback error")
if playbackFinished:
# Current paragraph finished, advance
if not self.next_paragraph():
self.displayText = "End of book reached"
self.isPlaying = False
self.save_bookmark(speakFeedback=False)
else:
# Start next paragraph with error recovery
try:
self._start_paragraph_playback()
except Exception as e:
print(f"Error starting playback: {e}")
self.speechEngine.speak("Playback error")
self.isPlaying = False
# Render the screen
self._render_screen()
@@ -807,18 +800,10 @@ class BookReader:
if readerEngine == 'speechd':
self.readingEngine.cancel_reading()
else:
# Stop audio player (handles both TTS and audio books)
self.audioPlayer.stop()
# Clean up TTS mpv subprocess if it's still running
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None:
try:
self.ttsMpvProcess.terminate()
self.ttsMpvProcess.wait(timeout=2)
except subprocess.TimeoutExpired:
print("Warning: TTS subprocess didn't terminate, force killing")
self.ttsMpvProcess.kill()
except Exception as e:
print(f"Error cleaning up TTS subprocess: {e}")
if self.audioPlayer.is_audio_file_loaded():
self.audioPlayer.stop_audio_file()
# Close Audiobookshelf session if active
if self.sessionId and self.absClient:
@@ -919,13 +904,13 @@ class BookReader:
self.speechEngine.speak("Paused")
self.readingEngine.pause_reading()
else:
# Handle piper-tts pause/resume
# Handle piper-tts pause/resume (now uses audio file methods)
if self.audioPlayer.is_paused():
self.speechEngine.speak("Resuming")
self.audioPlayer.resume()
self.audioPlayer.resume_audio_file()
else:
self.speechEngine.speak("Paused")
self.audioPlayer.pause()
self.audioPlayer.pause_audio_file()
elif event.key == pygame.K_n:
if not self.book:
@@ -1985,17 +1970,13 @@ class BookReader:
readerEngine = self.config.get_reader_engine()
isAudioBook = self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook
if isAudioBook:
# Audio books: instant speed change via MpvPlayer
if isAudioBook or (readerEngine == 'piper' and self.isPlaying):
# Both audio books and Piper-TTS use MpvPlayer: instant speed change
self.audioPlayer.set_speed(newSpeed)
elif readerEngine == 'piper' and self.isPlaying:
# Piper-TTS: restart current paragraph with new speed
# Stop current subprocess
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None:
self.ttsMpvProcess.terminate()
self.ttsMpvProcess.wait(timeout=0.5)
# Restart playback of current paragraph
self._start_paragraph_playback()
# For Piper-TTS, restart current paragraph to apply new speed immediately
if not isAudioBook and self.isPlaying:
self.audioPlayer.stop_audio_file()
self._start_paragraph_playback()
# Speak feedback
speedPercent = int(newSpeed * 100)
@@ -2066,11 +2047,11 @@ class BookReader:
self.config.set_last_book(bookPath)
self.config.set_books_directory(str(self.bookPath.parent))
# Reset position
self.currentChapter = 0
self.currentParagraph = 0
# Reset audio position state
self.savedAudioPosition = 0.0
self.bookmarkCleared = False
# Load new book
# Load new book (which will restore bookmark if it exists)
try:
self.load_book()
self.speechEngine.speak("Ready")
@@ -2088,28 +2069,17 @@ class BookReader:
isAudioBook = hasattr(self.book, 'isAudioBook') and self.book.isAudioBook
readerEngine = self.config.get_reader_engine()
if isAudioBook:
# Stop audio file playback
self.audioPlayer.stop_audio_file()
if isAudioBook or readerEngine == 'piper':
# Stop audio playback (audio books or TTS via MpvPlayer)
if readerEngine == 'piper':
self._cancel_buffer()
if self.audioPlayer.is_audio_file_loaded():
self.audioPlayer.stop_audio_file()
else:
self.audioPlayer.stop()
elif readerEngine == 'speechd':
# Cancel speech-dispatcher reading
self.readingEngine.cancel_reading()
else:
# Stop piper-tts playback and cancel buffering
self._cancel_buffer()
# Terminate the TTS mpv subprocess if it's running
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None:
try:
self.ttsMpvProcess.terminate()
self.ttsMpvProcess.wait(timeout=1)
except subprocess.TimeoutExpired:
# Force kill if it doesn't terminate gracefully
print("Warning: TTS subprocess didn't terminate, force killing")
self.ttsMpvProcess.kill()
self.ttsMpvProcess.wait(timeout=1)
except Exception as e:
print(f"Error terminating TTS subprocess: {e}")
self.audioPlayer.stop()
def _restart_current_paragraph(self):
"""
@@ -2177,67 +2147,24 @@ class BookReader:
wavData = self.ttsEngine.text_to_wav_data(paragraph)
if wavData:
# Stop any existing TTS mpv process
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None:
self.ttsMpvProcess.terminate()
self.ttsMpvProcess.wait()
# Stop any existing audio playback
if self.audioPlayer.is_audio_file_playing():
self.audioPlayer.stop_audio_file()
# Get audio parameters from TTS engine
audioParams = self.ttsEngine.get_audio_params()
sampleRate = audioParams['sampleRate']
sampleWidth = audioParams['sampleWidth']
channels = audioParams['channels']
# Determine mpv audio format string
# piper-tts outputs 16-bit signed PCM
mpvAudioFormat = 's16' # 16-bit signed integer
if channels == 2: # Stereo
mpvAudioFormat += 'le' # Little-endian (default for WAV)
# Launch mpv subprocess to read from stdin
# Get current playback speed from config
playbackSpeed = self.config.get_playback_speed()
mpvCmd = [
'mpv',
'--no-terminal',
f'--speed={playbackSpeed}',
'--', '-'
]
self.ttsMpvProcess = subprocess.Popen(
mpvCmd,
stdin=subprocess.PIPE
)
# Write WAV data to mpv's stdin in a separate thread
def write_mpv_stdin(process, data):
try:
process.stdin.write(data)
process.stdin.flush()
process.stdin.close()
# Explicitly delete data to free memory immediately
del data
except Exception as e:
print(f"Error writing WAV data to mpv stdin: {e}")
finally:
# Wait for mpv to finish and clean up the process
try:
process.wait()
except:
pass
threading.Thread(
target=write_mpv_stdin,
args=(self.ttsMpvProcess, wavData),
daemon=True
).start()
# 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
# Start buffering next paragraph in background
self._buffer_next_paragraph()
else:
print("Warning: No audio data generated")
except Exception as e:
@@ -2430,10 +2357,7 @@ class BookReader:
self.sessionId = None
self._cancel_buffer()
# Terminate the TTS mpv subprocess if it's running
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None:
self.ttsMpvProcess.terminate()
self.ttsMpvProcess.wait(timeout=1)
# Cleanup audio player (handles both TTS and audio books)
self.audioPlayer.cleanup()
self.speechEngine.cleanup()
if self.readingEngine:

View File

@@ -62,13 +62,55 @@ class MpvPlayer:
print(f"Warning: Could not initialize mpv: {e}")
self.isInitialized = False
def play_wav_data(self, wavData):
def play_wav_data(self, wavData, playbackSpeed=None):
"""
This method is no longer used for TTS playback.
TTS playback is now handled directly by BookReader using subprocess.
Play WAV data directly from memory (for TTS)
Args:
wavData: WAV file data as bytes
playbackSpeed: Playback speed (0.5 to 2.0), uses current speed if None
Returns:
True if playback started successfully
"""
print("Warning: MpvPlayer.play_wav_data is deprecated and should not be called.")
return False
if not self.isInitialized or not self.player:
return False
import tempfile
tempFile = None
try:
# Create a temporary file for the WAV data
# python-mpv needs a file path, it can't play from memory directly
tempFile = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
tempFile.write(wavData)
tempFile.close()
# Use current playback speed if not specified
if playbackSpeed is None:
playbackSpeed = self.playbackSpeed
# Load and play the temp file
success = self.load_audio_file(tempFile.name, playbackSpeed=playbackSpeed)
if success:
success = self.play_audio_file()
# Clean up temp file after a delay (mpv needs time to load it)
if tempFile:
import threading
import time
def cleanup_temp_file(filepath):
time.sleep(5) # Wait for mpv to fully load the file
try:
os.unlink(filepath)
except:
pass
threading.Thread(target=cleanup_temp_file, args=(tempFile.name,), daemon=True).start()
return success
except Exception as e:
print(f"Error playing WAV data: {e}")
return False
def pause(self):