<jupyter_start><jupyter_text>Buffered Text-to-SpeechIn this tutorial, we are going to build a state machine that controls a text-to-speech synthesis. The problem we solve is the following:- Speaking the text takes time, depending on how long the text is that the computer should speak.- Commands for speaking can arrive at any time, and we would like our state machine to process one of them at a time. So, even if we send three messages to it shortly after each other, it processes them one after the other.While solving this problem, we can learn more about the following concepts in STMPY state machines:- **Do-Activities**, which allow us to encapsulate the long-running text-to-speech function in a state machine.- **Deferred Events**, which allow us to ignore incoming messages until a later state, when we are ready again.  Text-to-Speech  MacOn a Mac, this is a function to make your computer speak:<jupyter_code>from os import system

def text_to_speech(text):
    system('say {}'.format(text))<jupyter_output><empty_output><jupyter_text>Run the above cell so the function is available in the following, and then execute the following cell to test it:<jupyter_code>text_to_speech("Hello. I am a computer.")<jupyter_output><empty_output><jupyter_text>WindowsTODO: We should have some code to run text to speech on Windows, too!  State Machine 1With this function, we can create our first state machine that accepts a message and then speaks out some text. (Let's for now ignore how we get the text into the method, we will do that later.)![](images/speaking/speaking-stm-1.png)Unfortunately, this state machine has a problem. This is because the method `text_to_speech(text)` is taking a long time to complete. This means, for the entire time that it takes to speak the text, nothing else can happen in all the state machines that are part of the same driver!  State Machine 2 Long-Running ActionsThe way this function is implented makes that it **blocks**. This means, the Python program is busy executing this function as long as the speech takes to pronouce the message. Longer message, longer blocking.You can test this by putting some debugging aroud the function, to see when the functions returns:<jupyter_code>print('Before speaking.')
text_to_speech("Hello. I am a computer.")
print('After speaking.')<jupyter_output><empty_output><jupyter_text>You see that the string _"After speaking"_ is printed after the speaking is finished. During the execution, the program is blocked and does not do anything else. When our program should also do other stuff at the same time, either completely unrelated to speech or even just accepting new speech commands, this is not working! The driver is now completely blocked with executing the speech method, not being able to do anything else.  Do-ActivitiesInstead of executing the method as part of a transition, we execute it as part of a state. This is called a **Do-Activity**, and it is declared as part of a state. The do-activity is started when the state is entered. Once the activity is finished, the state machine receives the event `done`, which triggers it to switch into another state.![](images/speaking/speaking-stm-2.png)You may think now that the do-activity is similar to an entry action, as it is started when entering a state. However, a do-activity is started as part of its own thread, so that it does not block any other behavior from happening. Our state machine stays responsive, and so does any of the other state machines that may be assigned to the same driver. This happens in the background, STMPY is creating a new thread for a do-activity, starts it, and dispatches the `done` event once the do-activity finishes.When the do-activity finishes (in the case of the text-to-speech function, this means when the computer is finished talking), the state machine dispatches _automatically_ the event `done`, which brings the state machine into the next state. - A state with a do activity can therefore only declare one single outgoing transition that is triggered by the event `done`. - A state can have at most one do-activity. - A do-activity cannot be aborted. Instead, it should be programmed so that the function itself terminates, indicated for instance by the change of a variable.The following things are still possible in a state with a do-activity:- A state with a do-activity can have entry and exit actions. They are simply executed before or after the do activities.- A state with a do-activity can have internal transitions, since they don't leave the state.<jupyter_code>from stmpy import Machine, Driver
from os import system

import logging
debug_level = logging.DEBUG
logger = logging.getLogger('stmpy')
logger.setLevel(debug_level)
ch = logging.StreamHandler()
ch.setLevel(debug_level)
formatter = logging.Formatter('%(asctime)s - %(name)-12s - %(levelname)-8s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)



class Speaker:
    def speak(self, string):
        system('say {}'.format(string))

speaker = Speaker()
        
t0 = {'source': 'initial', 'target': 'ready'}
t1 = {'trigger': 'speak', 'source': 'ready', 'target': 'speaking'}
t2 = {'trigger': 'done', 'source': 'speaking', 'target': 'ready'}

s1 = {'name': 'speaking', 'do': 'speak(*)'}

stm = Machine(name='stm', transitions=[t0, t1, t2], states=[s1], obj=speaker)
speaker.stm = stm

driver = Driver()
driver.add_machine(stm)
driver.start()

driver.send('speak', 'stm', args=['My first sentence.'])
driver.send('speak', 'stm', args=['My second sentence.'])
driver.send('speak', 'stm', args=['My third sentence.'])
driver.send('speak', 'stm', args=['My fourth sentence.'])

driver.wait_until_finished()<jupyter_output><empty_output><jupyter_text>The state machine 2 still has a problem, but this time another one: If we receive a new message with more text to speak _while_ we are in state `speaking`, this message is discarded. Our next state machine will fix this.  State Machine 3As you know, events arriving in a state that do not declare outgoing triggers with that event, are discarded (that means, thrown away). For our state machine 2 above this means that when we are in state `speaking` and a new message arrives, this message is discarded. However, what we ideally want is that this message is handled once the currently spoken text is finished. There are two ways of achieving this:1. We could build a queue variable into our logic, and declare a transition that puts any arriving `speak` message into that queue. Whenever the currently spoken text finishes, we take another one from the queue until the queue is empty again. This has the drawback that we need to code the queue ourselves.2. We use a mechanism called **deferred event**, which is part of the state machine mechanics. This is the one we are going to use below. Deferred EventsA state can declare that it wants to **defer** an event, which simply means to not handle it. For our speech state machine it means that state `speaking` can declare that it defers event `speak`. ![](images/speaking/speaking-stm-3.png)Any event that arrives in a state that defers it, is ignored by that state. It is as if it never arrived, or as if it is invisible in the incoming event queue. Only once we switch into a next state that does not defer it, it gets visible again, and then either consumed by a transition, or discarded if the state does not declare any transition triggered by it.<jupyter_code>s1 = {'name': 'speaking', 'do': 'speak(*)', 'speak': 'defer'}

stm = Machine(name='stm', transitions=[t0, t1, t2], states=[s1], obj=speaker)
speaker.stm = stm

driver = Driver()
driver.add_machine(stm)
driver.start()

driver.send('speak', 'stm', args=['My first sentence.'])
driver.send('speak', 'stm', args=['My second sentence.'])
driver.send('speak', 'stm', args=['My third sentence.'])
driver.send('speak', 'stm', args=['My fourth sentence.'])

driver.wait_until_finished()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08276729559748428, 'fraction_numerical': 0.003270440251572327, 'mean_word_length': 4.946896035901272, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '2029', 'license': 'MIT', 'n_tokens_mistral': 2127, 'n_tokens_neox': 2054, 'n_words': 1126, 'path': 'notebooks/Buffered Text-to-Speech.ipynb', 'repo_name': 'Hallvardd/ttm4115-project', 'size': '7950'}