How to make a Snips skill – Part 4 – Data to spoken text - what about multilanguage

31. December 2018 21:22
6 min reading
This post thumbnail

In the previous part we were looking at the text we want Snips to tell us trough TTS. Since my Blog posts are in English language and I’m speaking German back home I already translated and presented the examples in both languages. Hmm, this looks like a change in the goal to make a quick Snips App to learn all the basics. But like in real life with agile software development, we will add some features. In my company as the manager I would shout out change request, change request and tell the customer about the financial implication this decision will make. But this is a side project and I’m in my “end of year holidays” so we stop pondering and just do it. Here again the different parts of this walk trough journey.

Table of Content

  1. Overview
  2. Intents – how to get data for slots and intents
  3. All about API – get the needed public transport data
  4. Data to spoken text – what about Multilanguage
  5. Develop the actions – tell Snips what to do
  6. Put everything together – publish and debug the App and Actions

Data to spoken text

Let’s quickly look at the text we want to say and the raw data we get from the API again:

The raw data.

[{
"departure": "2019-01-12T13:07:00+0100",
"destination": "Laupen",
"origin": "Gümligen",
"platform": "3",
"stops": 13,
"transport": "S 2"
},
{
"departure": "2019-01-12T13:21:00+0100",
"destination": "Langnau i.E.",
"origin": "Gümligen",
"platform": "2",
"stops": 7,
"transport": "S 2"
},
{
"departure": "2019-01-12T13:24:00+0100",
"destination": "Thun",
"origin": "Gümligen",
"platform": "2",
"stops": 6,
"transport": "S 1"
},
{
"departure": "2019-01-12T13:33:00+0100",
"destination": "Fribourg/Freiburg",
"origin": "Gümligen",
"platform": "3",
"stops": 13,
"transport": "S 1"
}
]

The text that will be spoken in German:

Die nächste Verbindung von Gümligen ist der S 2 und fährt um 13:07 auf Gleis 3 nach Laupen. Er hält an 13 Haltestellen. Weitere Verbindungen sind: S 2 nach Langnau i.E. um 13:21 auf Gleis 2 mit 7 Stops; S 1 nach Thun um 13:24 auf Gleis 2 mit 6 Stops und S 1 nach Fribourg/Freiburg um 13:33 auf Gleis 3 mit 13 Stops.

And the English text:

The next connection from Gümligen is S 2 and leaves at 13:07 on platform 3 towards Laupen. There are 13 stops before the final destination. Other connections are: S 2 towards Langnau i.E. leaving 13:21 on platform 2 with 7 stops; S 1 towards Thun leaving 13:24 on platform 2 with 6 stops and S 1 towards Fribourg/Freiburg leaving 13:33 on platform 3 with 13 stops.

Maybe you noticed that I changed my wording already slightly from the last Blog part. Now I had the available data from the API and could adjust a bit.

Breaking down the text fragments

If we look closely at the text we can identify some unique fragments and some parts which are repeated, I will highlight the variable data:

  1. The next connection from Gümligen is S 2 and leaves at 13:07 on platform 3 towards Laupen.
  2. There are 13 stops before the final destination.
  3. Other connections are:
  4. S 2 towards Langnau i.E. leaving 13:21 on platform 2 with 7 stops;
  5. S 1 towards Thun leaving 13:24 on platform 2 with 6 stops
  6. and S 1 towards Fribourg/Freiburg leaving 13:33 on platform 3 with 13 stops.

Fragment number 1 and 2 are unique, the first sentences we speak are a bit more detailed. Number 3 is the filling sentence for the enumeration of the next connections. Number 4, 5 and 6 are almost similar. Number 4 has an end punctuation, number 5 none and number 6 starts and with “and” and has a punctuation at the end.

So with this knowledge we can go on and concatenate some strings in python and deliver it to Snips TTS engine. But hold on – Multilingual! Wouldn’t it be nice to let other users translate this App into their favorite language? I’m Swiss and we have already four languages here in our country, and no, English is not one of them – as you might have noticed already in my writing style 😄

Multilingual

I would love to let the user choose his preferred language when he installs the skill. The settings will be written into the config.inifile together with his favorite station for the “Time Table” questions. I’m also sure that Python has something to make a program available in multiple languages.

After some further research I found gettext a nice Python Module which is bundled with Python 2.7 and 3.x to help translate text in Applications. I also found a nice write-up from Al Sweigart on how to use this. There is also another tutorial worth reading from Theo.

So, here is a summary about what we have to do, to translate our text into many different languages:

  1. put all the strings we use inside a _() function
  2. once we have all our text in a module, run the pygettext.py Python program to generate a .pot file we can use to translate to other languages
  3. create the directory structure for the language support files
  4. download, install and run poedit
  5. load your .pot file
  6. translate the text into different languages
  7. safe the generated .po and .mo files into the language directory

Ok, let’s look at those different tasks a bit more closely!

The _() function of gettext

If you look at the source code of the get_station_board_text function on my GitHub repo you see how I concatenate the strings together:

# create the sentences
t_frag_1 = (
self._(
"The next connection from {origin} is {transport} and leaves at {departure} {platform} towards {destination}"
)
+ ". "
+ self._("There are {stops} stops before the final destination")
+ ". "
).decode("utf-8")

sentence = t_frag_1.format(**sb_data[0])

sentence += self._("Other connections are") + ": "

All the text which is inside the _() function will be translated at runtime into the language we choose when we instantiate our _Data2TextML class later. After some trial and error I found out, that it works best to put just text inside the _() functions and leave the punctuations outside. This helps a lot when we translate the sentences with Poedit. As you can see, I put all the variable text which will be filled with values from the API call into curly braces {} and fill in the variables with a call to .format(**sb_data[0]). We just have to make sure that the dictionary sb_data[0] contains all the elements we refer to in our text.

Extract all translatable text with pygettext.py

Before we can translate our text to other languages we need to grab it out of our source code somehow. This is where pygettext.py comes into play. Depending on your Python installation this Python program can be located in different places. On my Windows box I installed the Anaconda Distribution. With this distribution you can generate the .potfile with the following command:

C:\Users\myusername\AppData\Local\Continuum\anaconda3\Tools\i18n\pygettext.py -d snips __init__.py

Where __init__.py is the module where all my text concatenation is done and therefore all the _() functions are called. The output file will be called snips.pot

Create the directory structure for the language files

Since our translation will only be used within our own Snips App, we create the directory structure for the translation files in the top directory of the App.

locale
--de
----LC_MESSAGES
------snips.mo
------snips.po
--fr
----LC_MESSAGES
------snips.mo
------snips.po
...

For every language we want to be available we need to create the translations and store the output of Poedit into the locale/xy/LC_MESSAGES directory. For our initial language (en in our case) we don’t need a specific translation.

Translate the text with Poedit

Once we have the snips.potfile and the directory structure setup we can translate the texts with poedit

poedot Screenshot

So much for the Multilanguage functionality, easy isn’t it? Hmm, it took me a while to find out all the nitty gritty stuff till everything worked as expected. Especially one major pain aroused while I was testing the classes in my swiss_transport_info module. Which leads me to a short side track about the topic UNICODE.

Unicode in Python 2.7

Snips Apps are written in Python 2.7 (something one could could change maybe, but I wouldn’t know how yet). In this version of Python the handling of foreign characters is not as straight forwards as it could be. If you look at the different components our app has meanwhile, you may notice that we have to deal with a lot of foreign characters:

  • the names of Swiss Transport stations have “Umlauts” e.g. Zürich, so our slot values will have special characters.
  • the return values from our API will contain the same special characters
  • the return values from our text translations may also contain special characters (depending on the language we translate to)

While testing I tripped many times into an error like this:

UnicodeEncodeError: 'ascii' codec can't encode character u'\xfc' in position 0: ordinal not in range(128)

After reading trough a lot of StackOverflow posts and learning more about the handling of UNICODE in Python 2.7 I would like to give you some pointers to good explanations about this topic:

I somehow managed to get my code running without any errors. However I’m not absolute positive if I did it the correct, pythonic way. If you are into this more than I am, please let me know in the comments below.

In the next part of this Blog Series we are looking at how our Snips Intent gets routed to our App and how we can actually respond with the Text To Speech engine. Stay tuned.