2019-02-11 18:16:23

I have code that needs to wait for the text-to-speech engine to wait until no speech is being spoken. When I use Tolk.is_speaking() with NVDA, it won't report the state correctly. SAPI reports just fine.


I don't know what I'm missing here. Is there something better to use than Tolk that I just haven't found yet?


*** CODE ***


# Author: Stephen Luttrell
# File: testMessageBox.py
# Description: Tests the simple_message_box function in dynamic_menu.py * see below


from dynamic_menu import simple_message_box
from audio import shut_down_audio
from create_window import display
import pygame


display('test message box')
quit = True


box = simple_message_box()
box.set_text('If we desire to know what ideas men held in heathen times about the life beyond the grave, it is natural to turn first to the evidence of archaeology.')
box.set_confirm_button_text('mChange.wav')
box.set_speak_ok(False)
#box.set_text('intro.wav')
#box.set_is_filename(True)


while quit:
    for e in pygame.event.get():
        if e.type == pygame.KEYDOWN:
            if e.key == pygame.K_RETURN:
                box.run()
            elif e.key == pygame.K_ESCAPE:
                quit = False
shut_down_audio()
print('done')


# File: dynamic_menu.py
# Description: Creates an extensible menu and also some simple data gathering and display objects


from audio import *
import pygame
import time


# A message box that speaks or plays some message then waits for confirmation
class simple_message_box: # Object for a text box with a single dismissal action
    def __init__(self):
        self.text = '' # The body of the message
        self.confirmButton = 'ok' # The confirm alert when the message is done
        self.speakOK = True # True sends the text of the confirm button to the text-to-speech engine: False sends it to the sound device as a file path of a sound to be played
        self.playSound = False # False sends the contents of the message box to the text-to-speech engine: True sends it to the sound device as a file path to be played


    # Changes the message contents of the body to text to be spoken or the path of a file to be played
    def set_text(self, text):
        self.text = text


    # Changes the default text of the confirm button (or to the path of a file to be played)
    def set_confirm_button_text(self, text):
        self.confirmButton = text


    # Sets the text body to text to be spoken (False) or the path of a sound to be played (True)
    def set_is_filename(self, value):
        self.playSound = value


    # Sets the confirm button to text to be spoken (True) or the path of a sound to be played (False)
    def set_speak_ok(self, value):
        self.speakOK = value


    def run(self):
        # Method variables
        quit = True
        playOK = 0 # Tells the function when it's time to play or speak the confirm button (0: waiting to be played, 1: play the button, 2: has played the button and is not to play it again)


        source = play_text(self.text, self.playSound)


        while quit:
            time.sleep(.025) # Be kind to processors (This time it also serves to delay the loop from running long enough for the text-to-speech engine to report as active)
            sound_cleanup()
            if self.playSound: # This is a sound file
                if not is_playing(source): # The sound has finished playing
                    if not playOK == 2: # playOK tells the method to speak the confirm button or play a confirm sound when the text or audio message is finished. It will only play if playOK is 1 (setting it to 2 prevents it from triggering multiple times).
                        playOK = 1
            else: # This is a block of text
                if not is_speaking(): # The text-to-speech engine is finished with the text.
                    if not playOK == 2:
                        playOK = 1


            if playOK == 1: # Play the confirm sound or speak the confirm button
                if not self.speakOK: # This is a confirm sound
                    play_sound(self.confirmButton) # Play the sound once
                else: # This is a text button
                    speak(self.confirmButton) # Speak the confirm button using text-to-speech
                playOK = 2 # Don't play the confirm button again


            for e in pygame.event.get(): # Poll the key events
                if e.type == pygame.KEYDOWN:
                    if e.key == pygame.K_RETURN or e.key == pygame.K_ESCAPE or e.key == pygame.K_SPACE:
                        # Stop the sound/speech and exit the text box regardless if either is finished
                        if self.playSound: # This is a audio message
                            stop_sound(source) # Stop the sound
                        else:
                            stop_speaking()
                        quit = False
                    if e.key == pygame.K_UP: # Replay the contents
                        if self.playSound:
                            if not is_playing(source): # The contents have finished playing
                                playOK = 0 # Also replay the confirm button
                                source = play_text(self.text, self.playSound)
                        else:
                            if not is_speaking():
                                playOK = 0
                                speak(self.text)


# A helper function to replay sounds and text and such
def play_text(text, isFilename):
    if isFilename:
        return play_sound(text)
    else:
        speak(text)
        return 0


*** Skip to the bottom for the TTS functions ***


# File: audio.py
# Description: Various audio and tts functions


from openal import *
import pygame
import random
import time
import Tolk


pool = {} # Dictionary of sound sources


# adds the source file to the sound pool and Plays the audio file
def play_sound(filename):
    # Function variables
    global pool


    if not filename == "": # If there is a valid path to a sound that can be played
        index = time.time() # Generate an index based on the number of seconds since the beginning of the robot apocalypse began
        while index in pool: # Make sure the index does not already exist. Not likely, but possible. So this makes sure it can't happen.
            index = index + (index * (random.randint(1, 100)/100))
        pool = {index: oalOpen(filename)} # Add the source to the sound pool dictionary
        pool[index].play() # play the sound


        return index # returns the index for sound management


# Plays a company intro (skippable with any key)
def play_intro(filename='intro.wav'): # intro is the default file name (Change to reflect file structure)
    # Function variables
    global pool
    quit = False


    source = play_sound(filename) # Play the sound


    while not quit: # Loop until the intro is done playing
        time.sleep(.025) # Be kind to processors
        if not is_playing(source): # Intro is finished
            sound_cleanup() # Remove the source from the sound pool
            quit = True # End the loop


        for e in pygame.event.get(): # Query the event pool
            if e.type == pygame.KEYDOWN: # Any key
                stop_sound(source) # End the intro early and remove it from the sound pool
                quit = True # End the loop


# Returns the state of the audio source (whether it is playing or not)
def is_playing(index):
    # Function variables
    global pool


    if index in pool: # If the source still exists
        if pool[index].get_state() == AL_PLAYING: # The audio is playing
            return True


    return False # If the audio is not playing or no longer exists


# Stops a sound source from playing and removes it from the pool
def stop_sound(index):
    # Function variables
    global pool


    if index in pool: # If the source still exists
        if is_playing(index): # The sound is still playing
            pool[index].stop() # So stop it first


        pool.pop(index) # And remove it from the pool regardless


# Removes idle sounds from the sound pool
def sound_cleanup():
    global pool
    listOfIndexes = []


    for index in pool: # Loop through the indexes stored in the sound pool
        listOfIndexes.append(index) # Add it to the list for removal


    for index in listOfIndexes: # loop through the list of indexes
        if not is_playing(index): # The sound is not playing (idle)
            pool.pop(index) # So remove it from the pool


# Text-to-speech functions


# Speak the text
def speak(text):
    if not Tolk.is_loaded(): # load Tolk if it has not been already
        Tolk.try_sapi(True) # Default to SAPI if no other speech engine (e.g. NVDA) is available
#        Tolk.prefer_sapi(True) # Use SAPI as default speech engine
        Tolk.load()


    if len(text) == 1: # This is a single character
        if ord(text) >= 65 and ord(text) <= 90: # This is a capital letter
            text = 'cap ' + text # Add "cap" to the output to alert the user to upper case


    Tolk.output(text) # Send the text to Tolk to be spoken


# Returns the state of the text-to-speach engine
def is_speaking():
    return Tolk.is_speaking()


# Cancels the speach of the text-to-speech engine
def stop_speaking():
    Tolk.silence()


# Finalize and end all the audio and text-to-speech stuff
def shut_down_audio():
    # Function variables
    global pool


    for index in pool: # Make sure all the sounds, music, and such are stopped
        pool[index].stop()


    pool.clear() # Clear the sound pool from memory
    oalQuit() # Shutdown OpenAL devices
    Tolk.unload() # Close out text-to-speech

***
You can follow me on twitter @s_luttrell and an almost never used Facebook account at skluttrell.

2019-02-13 16:53:24

I guess I found my answer: https://github.com/nvaccess/nvda/issues/5638


It's not even a blip on their super low priorities list of things to do.


I wish I knew enough programming to contribute to the project, but I'm small time.

***
You can follow me on twitter @s_luttrell and an almost never used Facebook account at skluttrell.

2019-02-14 09:23:32

I know I will be kinda offtop yerky man here, but is anything on NV Access's priority list currently?
NVM, anyway, the mentioned TOLK function will just not work, no matter how much you'll try

If you want to contact me, do not use the forum PM. I respond once a year or two, when I need to write a PM myself. I apologize for the inconvenience.
Telegram: Nuno69a
E-Mail: nuno69a (at) gmail (dot) com

2019-02-14 14:00:38

LOL


I only bother checking when I need something.


I moved on from Tolk and NVDA anyway. I'll just use SAPI for the time being and I can control more with pyttsx3 than I can with Tolk's short list of functions it would seem.

***
You can follow me on twitter @s_luttrell and an almost never used Facebook account at skluttrell.