Développez votre assistant vocal pour être alerté d'une publication sur INpact Hardware

You are the new Neo
Développez votre assistant vocal pour être alerté d'une publication sur INpact Hardware
Crédits : ra2studio/iStock/Thinkstock

Le meilleur moyen pour comprendre, c'est parfois de faire. Une méthode qui nous semble adaptée à la complexité apparente des assistants vocaux. Dans ce premier guide, vous apprendrez à développer une application exploitant la synthèse vocale.

Les assistants vocaux et les appareils connectés qui les embarquent paraissent presque magiques à l'utilisateur. Pourtant, ils n'effectuent en général que de simples requêtes vers des serveurs, agissant comme de grosses boîtes noires.

Pour mieux le comprendre et démystifier le fonctionnement de ces produits et services, le plus simple est de vous expliquer comment créer les vôtres, étape par étape. Elles permettront de découvrir les nombreuses petites briques logicielles qui se cachent derrière cette tendance, parfois plus simples à mettre en œuvre qu'il n'y parait.

Après vous avoir accompagné dans l'installation de l'assistant local Snips, focalisons nous sur une étape particulière : la synthèse vocale (text-to-speech, tts). Pour cela, voici un petit guide sur la création d'un script et de modules Python pour Raspberry Pi (par exemple) vous alertant de la publication d'une nouvelle actualité sur INpact Hardware.

« Coucou, comment ça va ? »

Pour commencer ce guide, il vous faut un système avec Python installé et de quoi jouer du son. Pour cela, vous pouvez utiliser une distribution Linux, macOS ou Windows. Une connexion internet fonctionnelle est également nécessaire.

Nous avons opté pour un Raspberry Pi 3 A+ sous Raspbian Lite avec une enceinte branchée sur le port jack. Aucun micro n'est encore nécessaire, puisque nous évoquerons la reconnaissance vocale dans un prochain article. Pour tester le fonctionnement de l'enceinte, la commande suivante suffit :

speaker-test -c2 --t wav -w /usr/share/sounds/alsa/Front_Center.wav

Commençons par l'étape d'apparence la plus complexe, pourtant la plus simple : la synthèse vocale. Pour rappel, il existe de tels dispositifs depuis des années. Les premières Sound Blaster 16 et leur perroquet faisant la causette en sont un bon exemple. Avec Python, le module pyttsx peut être utilisé.

Pour cela, connectez-vous à votre Raspberry Pi ou lancez un terminal. Tapez les commandes suivantes :

mkdir ~/ih_news_checker
cd ~/ih_news_checker/
nano tts.py

Nous avons créé un répertoire ih_news_checker dans celui de l'utilisateur (représenté par un ~) puis nous y éditons un fichier tts.py avec le logiciel nano. Placez-y le script suivant :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pyttsx3
engine = pyttsx3.init()
engine.setProperty("voice", "french")
engine.setProperty("rate", 100)
engine.say("Coucou, comment ça va ?")
engine.runAndWait()

Enregistrez le résultat (CTRL+S) puis quittez (CTRL+X). Il faut ensuite installer le module pyttsx3 que nous utilisons dans le script, ce qui nous permettra de l'exécuter :

pip install pyttsx3
python tts.py

Le résultat est pour le moins assez... robotique, et décevant. Testons donc un second logiciel, pico2wave, qui produit un fichier WAV depuis une phrase fournie en paramètre. Son installation est réservée à Linux, via la commande suivante pour Debian et ses dérivés (dont Raspbian) :

sudo apt install libttspico-utils

Le test passe par deux lignes de commandes :

pico2wave -l fr-FR -w test.wav "Coucou, comment ça va ?"
aplay test.wav && rm test.wav

pico2wave -l fr-FR -w test.wav "Cool, je suis un Raspberry Pi qui parle !"
aplay test.wav && rm test.wav

On enchaîne ici trois étapes : la création du fichier, sa lecture via aplay puis sa suppression (rm).  Le résultat est bien meilleur, mais pas encore parfait. Surtout que sur certaines références de produits, par exemple contenant un « s » après une série de chiffres, la voix prononcera « seconde ».

C'est encore le souci des outils de synthèse vocale au fonctionnement local et librement accessibles, notamment lorsque l'on veut utiliser du texte en français. Une limitation que l'on peut tenter de contourner, mais pas sans conséquences.

En attendant des solutions efficaces et ouvertes...

Pour ajouter une couche de réalisme, les développeurs ont en général recours à un modèle entrainé par un réseau de neurones. Si Mozilla travaille sur un moteur ouvert au sein de son projet Common Voice, il n'est pas encore exploitable. Il en est de même pour Mimic 2 de Mycroft. De son côté, MaryTTS utilise une architecture client/serveur qui nécessite une installation et une mise en œuvre plutôt lourdes pour un petit projet.

On peut alors se tourner vers des services en ligne. Amazon (Polly), Google (TTS) et Microsoft (Speech Services) proposent des outils professionnels nécessitant la création d'un compte. Ils reposent généralement sur un modèle Freemium, avec une part de gratuité suffisante pour un usage personnel, puis une tarification passé un certain nombre de phrases.

Il existe une astuce officieuse permettant une sorte de compromis. Il s'agit d'utiliser une fonctionnalité non documentée de Google Translate. Le service expose une URL permettant d'obtenir un fichier audio depuis un texte, prononçable dans différentes langues, dont le français :

... gTTS pour la gestion simple du text-to-speech

Un module Python permet d'utiliser facilement cette possibilité : gTTS. Ici, le script génère un fichier MP3 depuis le texte fourni. Il faut donc utiliser une mécanique similaire à pico2wave pour créer le fichier puis le lire. Installons d'abord le module, puis de quoi lire le format MP3 :

pip install gtts
sudo apt install mpg321

On peut ensuite l'utiliser dans notre script Python tts.py, en laissant à l'utilisateur le choix du service à utiliser. Son contenu devient alors :

#!/usr/bin/env python
# -*- coding: utf-8 -*

import os
MODULE_TO_USE = "gtts"
TMP_FILE = "/tmp/tts_temp_sample"

def _gtts(phrase_to_read):
from gtts import gTTS
tts = gTTS(text=phrase_to_read, lang="fr")
tts.save(TMP_FILE + ".mp3")
os.system("mpg321 -q {}{}".format(TMP_FILE, ".mp3"))

def _pico2wave(phrase_to_read):
os.system("pico2wave -l fr-FR -w {}{} \"{}\"".format(TMP_FILE,
".wav", phrase_to_read.encode("utf-8")))
os.system("aplay -q {}{}".format(TMP_FILE, ".wav"))

def _pyttsx(phrase_to_read):
import pyttsx3
engine = pyttsx3.init()
engine.setProperty("voice", "french")
engine.setProperty("rate", 100)
engine.say(phrase_to_read)
engine.runAndWait()

def speak(phrase_to_pass):
if MODULE_TO_USE == "gtts": _gtts(phrase_to_pass)
elif MODULE_TO_USE == "pico": _pico2wave(phrase_to_pass)
else: _pyttsx(phrase_to_pass)

On importe le module os qui nous servira à l'exécution d'applications. On laisse ensuite l'utilisateur indiquer quel service il veut utililser avant de déclarer l'emplacement du fichier temporaire (à adapter selon le système).

Trois fonctions (def) viennent alors gérer les différents cas proposés. On reprend ici simplement le fonctionnement proposé depuis les documentations officielles. Notez que dans le cas de pico2wave, il faut lire en UTF-8 le texte transmis à une application en ligne de commande (os.system).

La fonction speak sert enfin à appeler notre script depuis un outil tiers. Il s'agit ainsi d'un module que d'autres pourront utiliser. On peut imaginer aller plus loin, en intégrant d'autres services en ligne ou en forçant l'utilisation d'une solution locale en remplacement de gTTS si la connexion Internet ne fonctionne pas.

Un module pour suivre les publications d'INpact Hardware

Passons maintenant à ih.py dont le rôle est de vérifier le flux RSS d'INpact Hardware pour savoir si une nouvelle publication a été mise en ligne. Notez qu'il peut facilement être adapté à n'importe quel autre flux RSS. Il prend lui aussi la forme d'un module devant ensuite être intégré à un script tiers.

Commencez par éditer le fichier :

nano ih.py

Placez-y ce premier bout de code permettant de télécharger le flux et d'en extraire les informations utiles :

#!/usr/bin/env python
# -*- coding: utf-8 -*

import xml.etree.ElementTree as tree
import urllib2
import json
import os

FILE_GUID = os.path.expanduser("~") + "/.ih_news_guid_list.txt"
URL_FEED = "https://api-v1.inpact-hardware.com/rss/news.xml"

def _check_last_items():
_items = tree.parse(urllib2.urlopen(URL_FEED)).getroot().iter("item")

_news = list()
_guids = list()
for i in _items:
_news.append({ "title":i.findtext("title"),
"link":i.findtext("link"),
"link_comments":i.findtext("comments"),
"guid":i.findtext("guid"),
"description":i.findtext("description"),
"creator":i.findtext("{http://purl.org/dc/elements/1.1/}creator"),
"date":i.findtext("pubDate"),
"image":i.find("enclosure").attrib["url"]})

_guids.append(i.findtext("guid"))

return {"news":_news,"guids":_guids}

Ici on importe les différents modules à utiliser, puis on déclare l'URL du flux RSS et l'emplacement d'un fichier où nous stockerons la liste des actualités déjà mises en ligne à travers leur identifiant unique (guid). Cela nous permettra de repérer les nouvelles.

La fonction _check_last_items récupère tout d'abord les <item> du flux RSS qui contiennent les informations de chaque publication. Pour chacune on extrait le titre, lien, identifiant, description, auteur, date de publication et URL de l'image. Ces éléments sont placés dans un dictionnaire puis ajoutées dans une liste.

Enfin, on créé une liste de tous les identifiants uniques récupérés (guid). On renvoie les deux listes dans un dictionnaire contenant deux éléments : news (le contenu des publications) et guids (leurs guid).

def _read_already_seen_guids():
result = list()

if os.path.isfile(FILE_GUID):
with open(FILE_GUID,"r") as f: result = f.readlines()

return result

La seconde fonction _read_already_seen_guids permet de récupérer la liste des guid déjà vus et stockés dans le fichier prévu à cet effet (FILE_GUID). Si le fichier existe, son contenu est lu puis placé dans la liste. Sinon, la liste est renvoyée vide.

def _extract_news(_feed_content, _guids_list):
_feed_guids_with_newline = ["%s\n" % guid for guid in _feed_content["guids"]]
_new_guids = list(set(_feed_guids_with_newline) - set(_guids_list))

_result = list()
for n in _feed_content["news"]:
if n["guid"] + "\n" in _new_guids: _result.append(n)

return _result

La fonction _extract_news permet de récupérer la liste des nouvelles publications à partir de deux éléments : une liste de celles contenues dans le flux et la liste des guid déjà vus, récupérée depuis le fichier prévu à cet effet. Notez que l'on rajoute parfois un « \n » pour la comparaison.

Ce caractère spécial est utilisé pour ajouter un saut de ligne à la fin de chaque guid, qui sera nécessaire lorsqu'il faudra stocker tous les éléments dans le fichier avec un guid par ligne.

On différencie les deux listes afin de récupérer celle des guid qui n'ont jamais été vus. Ensuite, on récupère les informations des publications correspondantes. On les ajoute à la liste servant de résultat, qu'on renvoie à la fin du processus.

def check_news():
_feed_items = _check_last_items()
_already_seen_guids = _read_already_seen_guids()

_new_news = list()
if len(_already_seen_guids) == 0:
_already_seen_guids = ["%s\n" % guid for guid in _feed_items["guids"]]
with open(FILE_GUID,"w") as f: f.writelines(_already_seen_guids)
else:
_new_news = _extract_news(_feed_items, _already_seen_guids)
for n in _new_news:
with open(FILE_GUID,"a") as f: f.writelines(n["guid"] + "\n")

if len(_already_seen_guids) > 500:
with open(FILE_GUID,"w") as f:
f.writelines(["%s\n" % guid for guid in _feed_items["guids"]])

return {"feed":_feed_items["news"],"new":_new_news}

On passe à la fonction principale, devant être utilisée depuis les scripts tiers : check_news. On exploite les fonctions précédentes pour récupérer les dernières publications puis la liste des guid déjà vus. Si cette dernière est vide, on la remplit, puis on écrit son contenu dans le fichier prévu, sans autre action. La liste des nouvelles publications restera donc vide.

Dans le cas contraire, on récupère la liste des nouvelles publications et on ajoute leurs guid dans le fichier prévu à cet effet. Si ce dernier dépasse les 500 entrées, il est nettoyé en y plaçant uniquement les dernières publications relevées. Enfin, on renvoie un dictionnaire contenant la liste des publications du flux (feed) et les nouvelles (new).

On termine ce module par une petite ligne assez simple :

if __name__ == "__main__": print json.dumps(check_news())

Celle-ci permet d'indiquer que si le script est lancé seul, pas depuis un script tiers, le résultat de la fonction check_news est renvoyé au format JSON. Sous Linux, cela permet de lancer l'une des deux commandes suivantes pour afficher le résultat formaté :

python ih.py | json_pp
python ih.py | jq

jq a l'avantage d'afficher des couleurs dans le résultat, afin de différencier le nom des variables et leurs valeurs. Mais il nécessite en général d'être installé. Sous Raspbian et les dérivés de Debian, cela passe par cette commande :

sudo apt install jq

Un script pour les gouverner tous

Nous disposons désormais de deux modules : l'un pour vérifier si de nouvelles publications ont été mises en ligne par INpact Hardware, l'autre pour transformer un texte en phrase prononcée. Il ne nous manque donc plus qu'un script pour réunir les deux. Ce sera le rôle de ih_news_checker.py.

Commencez par éditer le fichier :

nano ih_news_checker.py

Placez-y ensuite le code suivant :

#!/usr/bin/env python
# -*- coding: utf-8 -*

import tts
import ih
import os

news = ih.check_news()["new"]
text_to_read = None

if len(news) == 1:
text_to_read = "INpact Hardware a publié une nouvelle actualité"
elif len(news) > 1 :
text_to_read = "INpact Hardware a publié {} nouvelles actualités".format(len(news))

if text_to_read:
tts.speak(text_to_read.decode("utf-8"))
print text_to_read + " :"

for n in news:
tts.speak(n["title"])
print n["title"].encode("utf-8")
print n["link"].encode("utf-8")

On commence par importer nos deux modules en inscrivant leur nom (sans le .py). On utilise ensuite la fonction check_news du module ih pour récupérer les nouvelles publications (["new"]) que l'on place dans la variable news. On initialise une variable qui contiendra le texte à lire : text_to_read.

On compte ensuite les nouvelles publications. On identifie le cas où il y en a une ou plusieurs afin de gérer le pluriel. Si un texte est à lire, c'est qu'il y en a eu au moins une. On lit alors la phrase en gérant les éventuels caractères spéciaux (decode("utf-8")). On affiche également le résultat. On lit le titre de chaque nouvel élément, puis on affiche son titre et son lien.

Enregistrez le script puis rendez-le exécutable :

chmod a+rx ih_news_checker.py

Vous pourrez alors le lancer via la commande suivante (sous Linux), la première ligne du code permettant de préciser au système qu'il s'agit d'un script Python :

./ih_news_checker.py

Vous pouvez simuler une nouvelle publication en retirant quelques lignes au fichier des guid déjà relevés. Pour l'éditer, tapez la commande suivante :

nano ~/.ih_news_guid_list.txt

Une tâche Cron pour automatiser la vérification

Sous Linux, Cron permet d'exécuter un script de manière régulière. On peut l'exécuter toutes les cinq minutes, par exemple. Pour cela, il faut taper la commande suivante :

crontab -e

Cela ouvrira la liste des tâches planifiées. Si vous n'avez pas choisi d'éditeur, cela vous sera proposé (vous pouvez garder nano par défaut). Vous devrez ensuite ajouter la ligne suivante au fichier présenté :

*/5 * * * * export DISPLAY=:0 && ~/ih_news_checker/ih_news_checker.py 2>&1 | /usr/bin/logger -t ih_news_checker

« */5 » permet de demander une execution toutes les cinq minutes. « export DISPLAY=:0 && » permet d'indiquer que le script doit être lancé dans l'interface principale de la machine. « 2>&1 | /usr/bin/logger -t ih_news_checker » permet de rediriger la sortie de l'application vers le fichier de log du système avec un tag particulier.

Ainsi, pour afficher le résultat du script et les éventuelles erreurs, tapez cette ligne de commande :

grep ih_news_checker /var/log/syslog

Une indépendance à portée de main

Comme on vient de le voir, utiliser de la synthèse vocale dans un petit script est finalement plutôt simple. Il existe des solutions locales ou distantes, avec leurs avantages et inconvénients.

Tous ces outils peuvent être aisément interfacés avec d'autres applications, ici un lecteur de flux RSS. On pourrait faire de même avec des capteurs de température ou des objets connectés (via une API), de quoi disposer d'un retour vocal pour de nombreux services sans avoir à dépendre d'un service centralisé, d'un constructeur ou même d'un système en particulier.

Certes, il faudra mettre les mains dans le cambouis pour disposer de ce niveau de contrôle, mais cela se révèle être plutôt simple et intéressant. On peut d'ailleurs imaginer réaliser de petits projets en famille, de quoi mettre les enfants au code et à la gestion vocale de manière bien plus intéressante qu'en leur apprenant à écrire « Hello world » dans une page HTML ou à parler avec Alexa/Google Home.

L'intégralité du code de ce guide est disponible sur cette page.

Ce contenu est désormais en accès libre

Il a été produit grâce à nos abonnés, l'abonnement finance le travail de notre équipe de journalistes.

ou choisissez l'une de nos offres d'abonnement :

20 commentaires
Avatar de jackjack2 Abonné
Avatar de jackjack2jackjack2- 31/12/18 à 16:56:32

Super article! Décidément ça augure de très bonnes choses sur IH :yes:

Juste une remarque, il me semble (pas pratique de vérifier sur téléphone) que la version de Python n'est précisée nulle part, ça pourrait être utile pour les novices de préciser parce que ça peut réserver des surprises sur print, pip, byte stream, etc

Avatar de David_L Équipe
Avatar de David_LDavid_L- 31/12/18 à 17:04:36

(quote:38715:jackjack2) ...

Je fais en 2.7x pour éviter les mauvaises surprises, mais oui je vais voir pour préciser

Avatar de SLV17 Abonné
Avatar de SLV17SLV17- 31/12/18 à 17:43:32

Eh bien comme j’ai quelques rpi disponibles je sent que je vais en utiliser pour suivre vos tutoriels qui sortent de l’ordinaire. Merci pour la qualité de vos articles et l’explication donnée à chaques étapes. Moi qui ne suis pas développeur (seulement technicien d’exploitation) mais qui aime la bidouille depuis toujours. Vous me donnez l’occasion d’acquérir de nouvelles compétences !

Avatar de bilbonsacquet Abonné
Avatar de bilbonsacquetbilbonsacquet- 31/12/18 à 17:53:13

Intéressant tout ça ! Pour le flux RSS, est-ce qu'il existe une version "complète" pour les abonnés ?

Avatar de David_L Équipe
Avatar de David_LDavid_L- 31/12/18 à 18:05:46

(quote:38720:bilbonsacquet) Intéressant tout ça ! Pour le flux RSS, est-ce qu'il existe une version "complète" pour les abonnés ?

Pas encore mais ça viendra (faut qu'on voit comment on gère tout ça par rapport à ceux de NXi). Mais sur le fond, le dispositif mis en place peut permettre de lire les titres, ou des textes plus long (surtout avec des solutions faites pour ça comme Polly par exemple).

Du coup avec un peu de bidouille en plus on peut imaginer un dispositif qui lit les titres, et sur demande lit le contenu complet (mais bon avec un article comme celui-ci, bonne chance :D)

Avatar de Yutani Abonné
Avatar de YutaniYutani- 01/01/19 à 10:33:01

I'm Sorry Dave, I'm afraid i can't do that

ça a plus de classe, non ? :D

Édité par Yutani le 01/01/2019 à 10:36
Avatar de David_L Équipe
Avatar de David_LDavid_L- 01/01/19 à 10:45:48

(quote:38723:Yutani) ...

Ah ben après tout est fait pour que ce soit facilement exploitable pour tout et n'importe quoi (notamment le module tts, qu'on peut facilement adapter pour prendre une langue, un nom de service et une phrase en paramètre :p)

Cadeau

Édité par David_L le 01/01/2019 à 10:47
Avatar de Yutani Abonné
Avatar de YutaniYutani- 01/01/19 à 11:00:00

(quote:38724:David_L) Ah ben après tout est fait pour que ce soit facilement exploitable pour tout et n'importe quoi (notamment le module tts, qu'on peut facilement adapter pour prendre une langue, un nom de service et une phrase en paramètre :p)Cadeau

Merci M'sieur :smack:

Avatar de KaKi87 Abonné
Avatar de KaKi87KaKi87- 01/01/19 à 13:00:11

Manque plus que la coloration syntaxique ;)

Avatar de skankhunt42 Abonné
Avatar de skankhunt42 skankhunt42 - 01/01/19 à 16:32:50

Vous êtes sérieux avec ce genre d'article, du code et tout ? Ça va devenir mon deuxième site préféré ! A quand un peu de arduino et d'impression 3d ? :)

Il n'est plus possible de commenter cette actualité.
Page 1 / 2

2000 - 2019 INpact MediaGroup - SARL de presse, membre du SPIIL. N° de CPPAP 0321 Z 92244.

Marque déposée. Tous droits réservés. Mentions légales et contact