Text2speech

Exploiter la synthèse vocale

Accessibilité

Quand je parle de synthèse vocale, je parle de « text to speech » (T2S), ça sert à lire un texte à haute voix par l’ordinateur. Alors pour quelle utilité ? Et ben tout simplement pour augmenter l’accessibilité de ton site. A vrai dire, c’est surtout utilisé sur des sites qui sont destinés à être consultés par des personnes malvoyantes, comme pour un centre hospitalier par exemple.

élémentaire

Avant d’aller plus loin, je vais juste dire deux ou trois mots sur la mécanique et le service utilisés pour permettre de convertir le texte en bande son.

A la base, il y a un contenu HTML, ensuite il y a une action utilisateur, le plus souvent c’est un clic sur une icône « play », qui va déclencher une requête sur une page PHP. Sur cette page PHP, il va y avoir plusieurs opérations dont la demande de traduction du texte en bande son. Alors ici petite particularité, je vais utiliser le service Watson pour réaliser la conversion, et une fois le fichier .mp3 récupéré, il suffira de le renvoyer au JavaScript qui se chargera de le lire.

Structure

Je vais commencer par définir une structure, car je n’ai pas envie de lancer un T2S sur tout le contenu. Je vais d’une part créer différentes sections grâce à des balises div, d’autre part la classe t2s_section qui va me permettre d’identifier les contenus à lire. Le span injecté dans le titre h2 servira uniquement à afficher l’icône d’état (play/pause).

<div class="t2s_section">
   <h2>Songoku<span class="icon"></span></h2>
   <p>Kakarotto est un Saiyan né sur la planète Vegeta en l'an 737 de la chronologie de Dragon Ball. Il est le fils du guerrier Saiyan Baddack. Ce peuple guerrier travaillant pour Freezer était alors en première ligne dans la conquête des planètes habitables afin de bâtir l'empire du tyran qu'ils servaient.</p>
</div>
<div class="t2s_section">
   <h2>Vegeta<span class="icon"></span></h2>
   <p>Alors qu’il est enfant, tout le monde le suspecte d’être le guerrier légendaire, grâce à son potentiel de combat inné, et du fait qu’il est membre de la famille royale de la planète Vegeta. C’est pourquoi Freezer l’épargne lorsqu’il détruit la planète Vegeta. En effet, il voit en lui un parfait outil pour accomplir des missions de conquête. Il l’envoie régulièrement sur d’autres planètes avec Nappa, son compagnon d’arme et garde du corps. En grandissant, Vegeta n’a qu’un seul objectif : tuer Freezer, se libérer de son emprise et prendre sa place pour diriger l’univers. Mais il n’est pas encore assez fort pour rivaliser avec lui. Il subit d’ailleurs constamment les moqueries des lieutenants de Freezer, Kiwi, Zabon et Dodoria qui sont plus puissants que lui.</p>
</div>

Je vais également appeler un petit fichier CSS qui va me permettre d’ajouter un background au span et de le positionner sur la droite.

.icon.loading{
	background: url(icon_loading.png) transparent no-repeat;
}
.icon.play{
	background: url(icon_play.png) transparent no-repeat;
}
.icon.pause{
	background: url(icon_pause.png) transparent no-repeat;
}

Voici un petit aperçu du code sur une page web.

Clic

Maintenant, j’ajoute un peu de JavaScript pour réaliser le clic sur l’icône et je sauvegarde le this courant dans la variable el, car je sais que je vais devoir l’utiliser plus tard (pratique très courante). Enfin, je récupère aussi le contenu complet de la section grâce aux fonctions closest() et text() de jQuery.

$( ".t2s_section h2" ).on("click", ".icon", function(){
   var el = $(this);		
   var text = $(this).closest(".t2s_section").text();
});

Ensuite, je construis une requête ajax avec le texte passé en paramètres et je fais un coup de console pour vérifier le retour.

$( ".t2s_section h2" ).on("click", ".icon", function(){
   var el = $(this);		
   var text = $(this).closest(".t2s_section").text();

   jQuery.ajax({
      url: "audio.php",
      async: true,
      method: "POST", 
      data:{
      'action': 't2s',
      'text': text,
      success:function(response){
         console.log( response );
      }
   });
});

Conversion

Maintenant, je dois créer la page PHP sur laquelle je fais la requête ajax, nommée audio.php. Dessus, je vais simplement et grossièrement récupérer le texte, sans aucune autre validation, je vais vraiment me concentrer sur le t2s.

Pour effectuer la conversion du texte vers un fichier audio, je vais utiliser un service en ligne, le fameux service Watson. Ce qu’il y a de bien, c’est que c’est une API, donc il y a juste à faire une requête et le site retourne un fichier. Par contre, je dois formater le texte pour pouvoir le passer dans une URL, via la fonction urlencode(). Enfin, il suffit simplement de faire un file_get_contents() et d’attendre le retour de l’API.

$text = urlencode( $_POST["text"] );
$url = "https://watson-api-explorer.mybluemix.net/text-to-speech/api/v1/synthesize?accept=audio%2Fmp3&voice=fr-FR_ReneeVoice&text=" . $text;
$result = file_get_contents( $url );

Une fois le fichier récupéré, je vais le stocker pour pouvoir le réutiliser et ne pas faire plusieurs appels sur l’API. Pour ça, il y a une astuce pas mal, je vais utiliser une fonction de hackage comme md5 par exemple, que je vais exécuter sur le texte et dont le résultat sera le nom du fichier. L’avantage, c’est que je vais pouvoir tester si ce texte a déjà été traité en vérifiant si le hash correspond à un nom de fichier déjà existant. Si c’est le cas, alors je retourne l’URL du fichier.

//récupération du texte et urlencode
$text = urlencode( $_POST["text"] );

//nom du fichier
$fileName = md5($text) . ".mp3";

//chemin pour enregistrement du fichier
$pathFile = "audio" . DIRECTORY_SEPARATOR . $fileName;
	
if( file_exists($pathFile) ){
   //le fichier existe deja, retour de l'URL du fichier
   echo json_encode( array( "url" => $fileName ) );
}else{
   //préparation de l'URL pour l'API
   $url = "https://watson-api-explorer.mybluemix.net/text-to-speech/api/v1/synthesize?accept=audio%2Fmp3&voice=fr-FR_ReneeVoice&text=" . $text;
   
   //récupération du fichier mp3
   $result = file_get_contents($url);
	
   //stockage du fichier et retour de l'URL
   if( file_put_contents($pathFile, $result) ){
      echo json_encode( array( "url" => $fileName ) );
   }
}

Retour

Côté client, au-dessus de l’événement clic, je vais créer une variable audio me permettant de stocker le player audio qui sera en fait un HTMLAudioElement. Ensuite, au niveau du success de la requête ajax, je vais vérifier que la propriété url existe bien. Si c’est le cas, alors je crée un nouveau objet audio avec comme paramètre l’URL de la bande son, et je balance un play.

var audio = null;

$( ".t2s_section h2" ).on("click", ".icon", function(){
   var el = $(this);		
   var text = $(this).closest(".t2s_section").text();

   jQuery.ajax({
      url: "audio.php",
      async: true,
      method: "POST", 
      data:{
      'action': 't2s',
      'text': text,
      success:function(response){
            if( response.url != undefined && response.url != "" ){
	      audio = new Audio( response.url );
	      audio.controls = false;
	      audio.play();
            }
      }
   });
});

Etat

Le système tel quel est déjà fonctionnel, mais maintenant ce qui serait vraiment bien, c’est de pouvoir gérer les différents états de lecture. Il peut y en avoir 4, l’icône de base (icon_hear) qui incite la personne à cliquer, l’icône de loading lorsqu’il y a une requête faite au serveur, l’icône play pour lancer la lecture et enfin l’icône pause pour mettre en pause…

Je crée les icônes et j’adapte mon fichier CSS pour gérer tout ça avec des classes.

.icon.loading{
	background: url(icon_loading.png) transparent no-repeat;
}
.icon.play{
	background: url(icon_play.png) transparent no-repeat;
}
.icon.pause{
	background: url(icon_pause.png) transparent no-repeat;
}

Lors d’un clic sur l’icône, je vais détecter l’état courant et définir les actions à réaliser pour ensuite changer l’icône en modifiant sa classe.

Si audio est différent de null, c’est qu’un son a été ajouté et lancé, donc je mets en pause.

if( audio != null ){
	audio.pause();
}

Si l’élément possède la classe pause, ça veut dire que c’est l’icône pause qui est affichée, donc un clic dessus entraine la pause de la bande son. Inversement, si c’est le bouton play qui est affiché, alors on veut lancer la bande son. Il faut aussi supprimer et ajouter les classes en fonction de l’action.

if( $(el).hasClass("pause") ){
	audio.pause();
	$(el).removeClass("loading pause").addClass("play");
}else if( $(el).hasClass("play") ){
	audio.play();
	$(el).removeClass("loading play").addClass("pause");
}

L’icône du loading va être ajouté lors de l’appel de la méthode beforeSend sur la requête ajax et supprimé lors de la réception du fichier, voici le code du « clic » dans son entièreté.

var audio = null;

$( ".t2s_section h2" ).on("click", ".icon", function(){
	
	var el = $(this);
	
	if( audio != null ){
		audio.pause();
	}
	
	if( $(el).hasClass("pause") ){
		audio.pause();
		$(el).removeClass("loading pause playing").addClass("play");
	}else if( $(el).hasClass("play") ){
		audio.play();
		$(el).removeClass("loading play playing").addClass("pause");	
	}else{			

	   var el = $(this);		
	   var text = $(this).closest(".t2s_section").text();
		
	   jQuery.ajax({
	      url: "audio.php",
	      async: true,
	      method: "POST", 
	      data:{
	      'action': 't2s',
	      'text': text,
	      beforeSend:function(){
		     $(el).removeClass("pause play").addClass("loading");
		  },
	      success:function(response){
	            if( response.url != undefined && response.url != "" ){
			audio = new Audio( response.url );
			audio.controls = false;
			audio.play();
			$(el).removeClass("loading play").addClass("pause");
	            }
	      }
	   });
	   
	}
   
});

Résultat

Voici un petit aperçu de ce que ça donne graphiquement à l’écran (attention il n’y aura pas de son).

J’ai aussi créé un plugin WordPress plus complet sur github qui peut être téléchargé.

Note : Watson ne supporte plus file_get_contents, j’ai donc adapté le plugin avec curl.

05/02/2018

Yann Vangampelaere - nouslesdevs -

Sinon jete un coup d’oeil aux autres catégories

Ma boîte aux lettres

Tu veux me demander quelque chose ?