#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Speech handling for Storm Games. Provides functionality for: - Text-to-speech using different speech providers - Speech delay control to prevent stuttering - On-screen text display """ import pygame import textwrap import time from sys import exit class Speech: """Handles text-to-speech functionality.""" _instance = None @classmethod def get_instance(cls): """Get or create the singleton instance.""" if cls._instance is None: cls._instance = Speech() return cls._instance def __init__(self): """Initialize speech system with available provider.""" # Handle speech delays so we don't get stuttering self.lastSpoken = {"text": None, "time": 0} self.speechDelay = 250 # ms # Try to initialize a speech provider self.provider = None self.providerName = None # Try speechd first try: import speechd self.spd = speechd.Client() self.provider = self.spd self.providerName = "speechd" return except ImportError: pass # Try accessible_output2 next try: import accessible_output2.outputs.auto self.ao2 = accessible_output2.outputs.auto.Auto() self.provider = self.ao2 self.providerName = "accessible_output2" return except ImportError: pass # No speech providers found print("No speech providers found.") def speak(self, text, interrupt=True): """Speak text using the configured speech provider and display on screen. Args: text (str): Text to speak and display interrupt (bool): Whether to interrupt current speech (default: True) """ if not self.provider: return currentTime = pygame.time.get_ticks() # Check if this is the same text within the delay window if (self.lastSpoken["text"] == text and currentTime - self.lastSpoken["time"] < self.speechDelay): return # Update last spoken tracking self.lastSpoken["text"] = text self.lastSpoken["time"] = currentTime # Proceed with speech if self.providerName == "speechd": if interrupt: self.spd.cancel() self.spd.say(text) elif self.providerName == "accessible_output2": self.ao2.speak(text, interrupt=interrupt) # Display the text on screen screen = pygame.display.get_surface() if not screen: return font = pygame.font.Font(None, 36) # Wrap the text maxWidth = screen.get_width() - 40 # Leave a 20-pixel margin on each side wrappedText = textwrap.wrap(text, width=maxWidth // font.size('A')[0]) # Render each line textSurfaces = [font.render(line, True, (255, 255, 255)) for line in wrappedText] screen.fill((0, 0, 0)) # Clear screen with black # Calculate total height of text block totalHeight = sum(surface.get_height() for surface in textSurfaces) # Start y-position (centered vertically) currentY = (screen.get_height() - totalHeight) // 2 # Blit each line of text for surface in textSurfaces: textRect = surface.get_rect(center=(screen.get_width() // 2, currentY + surface.get_height() // 2)) screen.blit(surface, textRect) currentY += surface.get_height() pygame.display.flip() def close(self): """Clean up speech resources.""" if self.providerName == "speechd": self.spd.close() # Global instance for backward compatibility _speechInstance = None def speak(text, interrupt=True): """Speak text using the global speech instance. Args: text (str): Text to speak and display interrupt (bool): Whether to interrupt current speech (default: True) """ global _speechInstance if _speechInstance is None: _speechInstance = Speech.get_instance() _speechInstance.speak(text, interrupt) def messagebox(text): """Display a simple message box with text. Shows a message that can be repeated until the user chooses to continue. Args: text (str): Message to display """ speech = Speech.get_instance() speech.speak(text + "\nPress any key to repeat or enter to continue.") while True: event = pygame.event.wait() if event.type == pygame.KEYDOWN: if event.key in (pygame.K_ESCAPE, pygame.K_RETURN): speech.speak(" ") return speech.speak(text + "\nPress any key to repeat or enter to continue.")