Huge refactor of the libstormgames library. It is hopefully mostly backwards compatible. Still lots of testing to do, and probably some fixes needed, but this is a good start.
This commit is contained in:
149
speech.py
Normal file
149
speech.py
Normal file
@ -0,0 +1,149 @@
|
||||
#!/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.last_spoken = {"text": None, "time": 0}
|
||||
self.speech_delay = 250 # ms
|
||||
|
||||
# Try to initialize a speech provider
|
||||
self.provider = None
|
||||
self.provider_name = None
|
||||
|
||||
# Try speechd first
|
||||
try:
|
||||
import speechd
|
||||
self.spd = speechd.Client()
|
||||
self.provider = self.spd
|
||||
self.provider_name = "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.provider_name = "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
|
||||
|
||||
current_time = pygame.time.get_ticks()
|
||||
|
||||
# Check if this is the same text within the delay window
|
||||
if (self.last_spoken["text"] == text and
|
||||
current_time - self.last_spoken["time"] < self.speech_delay):
|
||||
return
|
||||
|
||||
# Update last spoken tracking
|
||||
self.last_spoken["text"] = text
|
||||
self.last_spoken["time"] = current_time
|
||||
|
||||
# Proceed with speech
|
||||
if self.provider_name == "speechd":
|
||||
if interrupt:
|
||||
self.spd.cancel()
|
||||
self.spd.say(text)
|
||||
elif self.provider_name == "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
|
||||
max_width = screen.get_width() - 40 # Leave a 20-pixel margin on each side
|
||||
wrapped_text = textwrap.wrap(text, width=max_width // font.size('A')[0])
|
||||
# Render each line
|
||||
text_surfaces = [font.render(line, True, (255, 255, 255)) for line in wrapped_text]
|
||||
screen.fill((0, 0, 0)) # Clear screen with black
|
||||
# Calculate total height of text block
|
||||
total_height = sum(surface.get_height() for surface in text_surfaces)
|
||||
# Start y-position (centered vertically)
|
||||
current_y = (screen.get_height() - total_height) // 2
|
||||
# Blit each line of text
|
||||
for surface in text_surfaces:
|
||||
text_rect = surface.get_rect(center=(screen.get_width() // 2, current_y + surface.get_height() // 2))
|
||||
screen.blit(surface, text_rect)
|
||||
current_y += surface.get_height()
|
||||
pygame.display.flip()
|
||||
|
||||
def close(self):
|
||||
"""Clean up speech resources."""
|
||||
if self.provider_name == "speechd":
|
||||
self.spd.close()
|
||||
|
||||
# Global instance for backward compatibility
|
||||
_speech_instance = 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 _speech_instance
|
||||
if _speech_instance is None:
|
||||
_speech_instance = Speech.get_instance()
|
||||
_speech_instance.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.")
|
Reference in New Issue
Block a user