First pass at implementing a speech history option for games.
This commit is contained in:
11
__init__.py
11
__init__.py
@@ -15,7 +15,8 @@ This module provides core functionality for Storm Games including:
|
|||||||
from .services import (
|
from .services import (
|
||||||
ConfigService,
|
ConfigService,
|
||||||
VolumeService,
|
VolumeService,
|
||||||
PathService
|
PathService,
|
||||||
|
SpeechHistoryService
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import Sound class and functions
|
# Import Sound class and functions
|
||||||
@@ -39,7 +40,7 @@ from .sound import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Import Speech class and functions
|
# Import Speech class and functions
|
||||||
from .speech import messagebox, speak, Speech
|
from .speech import messagebox, speak, Speech, speak_previous, speak_current, speak_next
|
||||||
|
|
||||||
# Import Scoreboard
|
# Import Scoreboard
|
||||||
from .scoreboard import Scoreboard
|
from .scoreboard import Scoreboard
|
||||||
@@ -78,7 +79,7 @@ __version__ = '2.0.0'
|
|||||||
# Make all symbols available at the package level
|
# Make all symbols available at the package level
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Services
|
# Services
|
||||||
'ConfigService', 'VolumeService', 'PathService',
|
'ConfigService', 'VolumeService', 'PathService', 'SpeechHistoryService',
|
||||||
|
|
||||||
# Sound
|
# Sound
|
||||||
'Sound',
|
'Sound',
|
||||||
@@ -102,6 +103,9 @@ __all__ = [
|
|||||||
'messagebox',
|
'messagebox',
|
||||||
'speak',
|
'speak',
|
||||||
'Speech',
|
'Speech',
|
||||||
|
'speak_previous',
|
||||||
|
'speak_current',
|
||||||
|
'speak_next',
|
||||||
|
|
||||||
# Scoreboard
|
# Scoreboard
|
||||||
'Scoreboard',
|
'Scoreboard',
|
||||||
@@ -133,6 +137,7 @@ __all__ = [
|
|||||||
configService = ConfigService.get_instance()
|
configService = ConfigService.get_instance()
|
||||||
volumeService = VolumeService.get_instance()
|
volumeService = VolumeService.get_instance()
|
||||||
pathService = PathService.get_instance()
|
pathService = PathService.get_instance()
|
||||||
|
speechHistoryService = SpeechHistoryService.get_instance()
|
||||||
|
|
||||||
# Set up backward compatibility hooks for initialize_gui
|
# Set up backward compatibility hooks for initialize_gui
|
||||||
_originalInitializeGui = initialize_gui
|
_originalInitializeGui = initialize_gui
|
||||||
|
134
services.py
134
services.py
@@ -6,10 +6,13 @@ Provides centralized services to replace global variables:
|
|||||||
- ConfigService: Manages game configuration
|
- ConfigService: Manages game configuration
|
||||||
- VolumeService: Handles volume settings
|
- VolumeService: Handles volume settings
|
||||||
- PathService: Manages file paths
|
- PathService: Manages file paths
|
||||||
|
- SpeechHistoryService: Manages speech history for navigation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
from collections import deque
|
||||||
from xdg import BaseDirectory
|
from xdg import BaseDirectory
|
||||||
|
|
||||||
# For backward compatibility
|
# For backward compatibility
|
||||||
@@ -272,3 +275,134 @@ class PathService:
|
|||||||
globalPath = self.globalPath
|
globalPath = self.globalPath
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class SpeechHistoryService:
|
||||||
|
"""Speech history management service for message navigation."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_instance(cls):
|
||||||
|
"""Get or create the singleton instance."""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = SpeechHistoryService()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self, max_size=10):
|
||||||
|
"""Initialize speech history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_size (int): Maximum number of messages to keep in history
|
||||||
|
"""
|
||||||
|
self.history = deque(maxlen=max_size)
|
||||||
|
self.current_index = -1
|
||||||
|
self.max_size = max_size
|
||||||
|
|
||||||
|
def add_message(self, text):
|
||||||
|
"""Add a message to the speech history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The spoken message to add to history
|
||||||
|
"""
|
||||||
|
if not text or not text.strip():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add message with timestamp
|
||||||
|
message_entry = {
|
||||||
|
'text': text.strip(),
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.history.append(message_entry)
|
||||||
|
# Reset current index when new message is added
|
||||||
|
self.current_index = -1
|
||||||
|
|
||||||
|
def get_current(self):
|
||||||
|
"""Get the current message in history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str or None: Current message text, or None if no history
|
||||||
|
"""
|
||||||
|
if not self.history:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.current_index == -1:
|
||||||
|
# Return most recent message
|
||||||
|
return self.history[-1]['text']
|
||||||
|
else:
|
||||||
|
# Return message at current index
|
||||||
|
if 0 <= self.current_index < len(self.history):
|
||||||
|
return self.history[self.current_index]['text']
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def move_previous(self):
|
||||||
|
"""Navigate to the previous message in history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str or None: Previous message text, or None if at beginning
|
||||||
|
"""
|
||||||
|
if not self.history:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.current_index == -1:
|
||||||
|
# Start from the most recent message
|
||||||
|
self.current_index = len(self.history) - 1
|
||||||
|
elif self.current_index > 0:
|
||||||
|
# Move backward in history
|
||||||
|
self.current_index -= 1
|
||||||
|
else:
|
||||||
|
# Already at the beginning, wrap to the end
|
||||||
|
self.current_index = len(self.history) - 1
|
||||||
|
|
||||||
|
return self.get_current()
|
||||||
|
|
||||||
|
def move_next(self):
|
||||||
|
"""Navigate to the next message in history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str or None: Next message text, or None if at end
|
||||||
|
"""
|
||||||
|
if not self.history:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.current_index == -1:
|
||||||
|
# Already at most recent, wrap to beginning
|
||||||
|
self.current_index = 0
|
||||||
|
elif self.current_index < len(self.history) - 1:
|
||||||
|
# Move forward in history
|
||||||
|
self.current_index += 1
|
||||||
|
else:
|
||||||
|
# At the end, wrap to beginning
|
||||||
|
self.current_index = 0
|
||||||
|
|
||||||
|
return self.get_current()
|
||||||
|
|
||||||
|
def clear_history(self):
|
||||||
|
"""Clear all messages from history."""
|
||||||
|
self.history.clear()
|
||||||
|
self.current_index = -1
|
||||||
|
|
||||||
|
def set_max_size(self, max_size):
|
||||||
|
"""Change the maximum history size.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_size (int): New maximum size for history buffer
|
||||||
|
"""
|
||||||
|
if max_size > 0:
|
||||||
|
self.max_size = max_size
|
||||||
|
# Create new deque with new max size, preserving recent messages
|
||||||
|
old_history = list(self.history)
|
||||||
|
self.history = deque(old_history[-max_size:], maxlen=max_size)
|
||||||
|
# Adjust current index if needed
|
||||||
|
if self.current_index >= len(self.history):
|
||||||
|
self.current_index = len(self.history) - 1
|
||||||
|
|
||||||
|
def get_history_size(self):
|
||||||
|
"""Get the current number of messages in history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Number of messages in history
|
||||||
|
"""
|
||||||
|
return len(self.history)
|
||||||
|
86
speech.py
86
speech.py
@@ -61,12 +61,13 @@ class Speech:
|
|||||||
# No speech providers found
|
# No speech providers found
|
||||||
print("No speech providers found.")
|
print("No speech providers found.")
|
||||||
|
|
||||||
def speak(self, text, interrupt=True):
|
def speak(self, text, interrupt=True, priority="normal"):
|
||||||
"""Speak text using the configured speech provider and display on screen.
|
"""Speak text using the configured speech provider and display on screen.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text (str): Text to speak and display
|
text (str): Text to speak and display
|
||||||
interrupt (bool): Whether to interrupt current speech (default: True)
|
interrupt (bool): Whether to interrupt current speech (default: True)
|
||||||
|
priority (str): Speech priority - "important", "normal", or "notification"
|
||||||
"""
|
"""
|
||||||
if not self.provider:
|
if not self.provider:
|
||||||
return
|
return
|
||||||
@@ -82,13 +83,45 @@ class Speech:
|
|||||||
self.lastSpoken["text"] = text
|
self.lastSpoken["text"] = text
|
||||||
self.lastSpoken["time"] = currentTime
|
self.lastSpoken["time"] = currentTime
|
||||||
|
|
||||||
# Proceed with speech
|
# Add to speech history (import here to avoid circular imports)
|
||||||
|
try:
|
||||||
|
from .services import SpeechHistoryService
|
||||||
|
history_service = SpeechHistoryService.get_instance()
|
||||||
|
history_service.add_message(text)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Proceed with speech based on provider and priority
|
||||||
if self.providerName == "speechd":
|
if self.providerName == "speechd":
|
||||||
if interrupt:
|
if interrupt:
|
||||||
self.spd.cancel()
|
self.spd.cancel()
|
||||||
|
|
||||||
|
# Set priority for speechd
|
||||||
|
if priority == "important":
|
||||||
|
try:
|
||||||
|
import speechd
|
||||||
|
self.spd.set_priority(speechd.Priority.IMPORTANT)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
pass
|
||||||
|
elif priority == "notification":
|
||||||
|
try:
|
||||||
|
import speechd
|
||||||
|
self.spd.set_priority(speechd.Priority.NOTIFICATION)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
pass
|
||||||
|
else: # normal
|
||||||
|
try:
|
||||||
|
import speechd
|
||||||
|
self.spd.set_priority(speechd.Priority.TEXT)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
self.spd.say(text)
|
self.spd.say(text)
|
||||||
|
|
||||||
elif self.providerName == "accessible_output2":
|
elif self.providerName == "accessible_output2":
|
||||||
self.ao2.speak(text, interrupt=interrupt)
|
# For accessible_output2, use interrupt for important messages
|
||||||
|
use_interrupt = interrupt or (priority == "important")
|
||||||
|
self.ao2.speak(text, interrupt=use_interrupt)
|
||||||
|
|
||||||
# Display the text on screen
|
# Display the text on screen
|
||||||
screen = pygame.display.get_surface()
|
screen = pygame.display.get_surface()
|
||||||
@@ -121,17 +154,18 @@ class Speech:
|
|||||||
# Global instance for backward compatibility
|
# Global instance for backward compatibility
|
||||||
_speechInstance = None
|
_speechInstance = None
|
||||||
|
|
||||||
def speak(text, interrupt=True):
|
def speak(text, interrupt=True, priority="normal"):
|
||||||
"""Speak text using the global speech instance.
|
"""Speak text using the global speech instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text (str): Text to speak and display
|
text (str): Text to speak and display
|
||||||
interrupt (bool): Whether to interrupt current speech (default: True)
|
interrupt (bool): Whether to interrupt current speech (default: True)
|
||||||
|
priority (str): Speech priority - "important", "normal", or "notification"
|
||||||
"""
|
"""
|
||||||
global _speechInstance
|
global _speechInstance
|
||||||
if _speechInstance is None:
|
if _speechInstance is None:
|
||||||
_speechInstance = Speech.get_instance()
|
_speechInstance = Speech.get_instance()
|
||||||
_speechInstance.speak(text, interrupt)
|
_speechInstance.speak(text, interrupt, priority)
|
||||||
|
|
||||||
def messagebox(text, sounds=None):
|
def messagebox(text, sounds=None):
|
||||||
"""Enhanced messagebox with dialog support.
|
"""Enhanced messagebox with dialog support.
|
||||||
@@ -313,3 +347,45 @@ def _play_dialog_sound(entry, dialog_config, sounds):
|
|||||||
except Exception:
|
except Exception:
|
||||||
# Sound missing or error - continue silently without crashing
|
# Sound missing or error - continue silently without crashing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def speak_previous():
|
||||||
|
"""Navigate to and speak the previous message in speech history."""
|
||||||
|
try:
|
||||||
|
from .services import SpeechHistoryService
|
||||||
|
history_service = SpeechHistoryService.get_instance()
|
||||||
|
message = history_service.move_previous()
|
||||||
|
if message:
|
||||||
|
speak(message, interrupt=True, priority="important")
|
||||||
|
else:
|
||||||
|
speak("No previous messages", interrupt=True, priority="important")
|
||||||
|
except ImportError:
|
||||||
|
speak("Speech history not available", interrupt=True, priority="important")
|
||||||
|
|
||||||
|
|
||||||
|
def speak_current():
|
||||||
|
"""Repeat the current message in speech history."""
|
||||||
|
try:
|
||||||
|
from .services import SpeechHistoryService
|
||||||
|
history_service = SpeechHistoryService.get_instance()
|
||||||
|
message = history_service.get_current()
|
||||||
|
if message:
|
||||||
|
speak(message, interrupt=True, priority="important")
|
||||||
|
else:
|
||||||
|
speak("No current message", interrupt=True, priority="important")
|
||||||
|
except ImportError:
|
||||||
|
speak("Speech history not available", interrupt=True, priority="important")
|
||||||
|
|
||||||
|
|
||||||
|
def speak_next():
|
||||||
|
"""Navigate to and speak the next message in speech history."""
|
||||||
|
try:
|
||||||
|
from .services import SpeechHistoryService
|
||||||
|
history_service = SpeechHistoryService.get_instance()
|
||||||
|
message = history_service.move_next()
|
||||||
|
if message:
|
||||||
|
speak(message, interrupt=True, priority="important")
|
||||||
|
else:
|
||||||
|
speak("No next messages", interrupt=True, priority="important")
|
||||||
|
except ImportError:
|
||||||
|
speak("Speech history not available", interrupt=True, priority="important")
|
||||||
|
61
utils.py
61
utils.py
@@ -156,19 +156,76 @@ class Game:
|
|||||||
# No logo image found, just play audio
|
# No logo image found, just play audio
|
||||||
self.sound.cut_scene(audioKey)
|
self.sound.cut_scene(audioKey)
|
||||||
|
|
||||||
def speak(self, text, interrupt=True):
|
def speak(self, text, interrupt=True, priority="normal"):
|
||||||
"""Speak text using the speech system.
|
"""Speak text using the speech system.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text (str): Text to speak
|
text (str): Text to speak
|
||||||
interrupt (bool): Whether to interrupt current speech
|
interrupt (bool): Whether to interrupt current speech
|
||||||
|
priority (str): Speech priority - "important", "normal", or "notification"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Game: Self for method chaining
|
Game: Self for method chaining
|
||||||
"""
|
"""
|
||||||
self.speech.speak(text, interrupt)
|
self.speech.speak(text, interrupt, priority)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def speak_previous(self):
|
||||||
|
"""Navigate to and speak the previous message in speech history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Game: Self for method chaining
|
||||||
|
"""
|
||||||
|
from .speech import speak_previous
|
||||||
|
speak_previous()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def speak_current(self):
|
||||||
|
"""Repeat the current message in speech history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Game: Self for method chaining
|
||||||
|
"""
|
||||||
|
from .speech import speak_current
|
||||||
|
speak_current()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def speak_next(self):
|
||||||
|
"""Navigate to and speak the next message in speech history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Game: Self for method chaining
|
||||||
|
"""
|
||||||
|
from .speech import speak_next
|
||||||
|
speak_next()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def setup_speech_history_keys(self, previous_key=None, current_key=None, next_key=None):
|
||||||
|
"""Set up convenient key bindings for speech history navigation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
previous_key (int, optional): Key code for previous message (default: F1)
|
||||||
|
current_key (int, optional): Key code for current message (default: F2)
|
||||||
|
next_key (int, optional): Key code for next message (default: F3)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary mapping key codes to speech history functions
|
||||||
|
"""
|
||||||
|
# Set default keys if not provided
|
||||||
|
if previous_key is None:
|
||||||
|
previous_key = pygame.K_F1
|
||||||
|
if current_key is None:
|
||||||
|
current_key = pygame.K_F2
|
||||||
|
if next_key is None:
|
||||||
|
next_key = pygame.K_F3
|
||||||
|
|
||||||
|
# Return a dictionary that games can use in their event loops
|
||||||
|
return {
|
||||||
|
previous_key: self.speak_previous,
|
||||||
|
current_key: self.speak_current,
|
||||||
|
next_key: self.speak_next
|
||||||
|
}
|
||||||
|
|
||||||
def play_bgm(self, musicFile):
|
def play_bgm(self, musicFile):
|
||||||
"""Play background music.
|
"""Play background music.
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user