150 lines
4.7 KiB
Python
150 lines
4.7 KiB
Python
#!/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.")
|