libstormgames/speech.py

150 lines
4.9 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.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.")