Outils pour utilisateurs

Outils du site


numeriser_jean_de_la_fontaine

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentesRévision précédente
Prochaine révision
Révision précédente
numeriser_jean_de_la_fontaine [2022/03/04 15:24] Simon Deplatnumeriser_jean_de_la_fontaine [2022/03/07 16:10] (Version actuelle) Simon Deplat
Ligne 27: Ligne 27:
 **//pip// permet d'installer de manière simple les différentes librairies Python**. **Quand à l'//environnement virtuel//, il permet de faire cohabiter plusieurs projets Python en parallèle dont les librairies seraient contradictoires** (par exemple si deux versions différentes d'une même librairie sont nécessaires à deux projets). Plus d'info sur ces sujets, en anglais, [[https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/|ici]]. **//pip// permet d'installer de manière simple les différentes librairies Python**. **Quand à l'//environnement virtuel//, il permet de faire cohabiter plusieurs projets Python en parallèle dont les librairies seraient contradictoires** (par exemple si deux versions différentes d'une même librairie sont nécessaires à deux projets). Plus d'info sur ces sujets, en anglais, [[https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/|ici]].
  
-Après cela, une fois dans le bon dossier ( par exemple /home/machin/projets ), **nous clonons le répertoire** de Serge depuis //git//, ce qui nous facilite toute l'installation :+Après cela, une fois dans le bon dossier ( par exemple ///home/machin/projets// ), **nous clonons le répertoire** de Serge depuis //git//, ce qui nous facilite toute l'installation. Attention, il y a 2Go à télécharger, c'est un peu long :
 <code bash> <code bash>
 git clone https://github.com/sergeLabo/La_Fontaine git clone https://github.com/sergeLabo/La_Fontaine
Ligne 95: Ligne 95:
 from aitextgen import aitextgen from aitextgen import aitextgen
  
-ai2 = aitextgen(model_folder="trained_model",+ai = aitextgen(model_folder="trained_model",
                 tokenizer_file="aitextgen.tokenizer.json")                 tokenizer_file="aitextgen.tokenizer.json")
  
-ai2.generate(1, prompt="")+ai.generate(1, prompt="")
 </code> </code>
  
-Pour l'utiliser, je n'affiche pas le texte dans le terminal, mais le **redirige** vers un fichier nommé //texte.txt// :+Pour l'utiliser, je n'affiche pas le texte dans le terminal, mais le **redirige** vers un fichier nommé //text.txt// :
  
 <code bash> <code bash>
-./mon_env/bin/python3 test3.py > text.txt+./mon_env/bin/python3 textgen.py > text.txt
 </code> </code>
  
-Un exemple de texte généré :+**Un exemple de texte généré** :
  
 <code> <code>
Ligne 118: Ligne 118:
  
 **C'est du grand portnawak, c'est parfait.** **C'est du grand portnawak, c'est parfait.**
-** + 
-[EN CONSTRUCTION]**+=====Vocalisation===== 
 + 
 +Maintenant que le  plus important est fait, à savoir, d'une certaine manière, avoir mis en place le contexte cérébral de ce petit Jean, il faut commencer à l'**incarner**. 
 + 
 +La première étape est de pouvoir **le faire dicter ses nouvelles fables**, en utilisant **un logiciel de synthèse sonore**, en anglais //Text To Speech//, souvent abrévié //TTS//. 
 + 
 +**La ressource principale** que j'ai trouvée, sur ce sujet et concernant les logiciels libres, est [[https://doc.ubuntu-fr.org/synthese_vocale|la documentation Ubuntu]]. Cependant, Manu a également trouvé un logiciel pertinent développé par une fondation bien connue : //[[https://github.com/mozilla/TTS|Mozilla TTS]]//. 
 + 
 +De manière succincte, voici les résultats auxquels j'ai abouti pour ces logiciels. Je n'ai pas essayé //festival//
 + 
 +Premièrement, //espeak//. C'est presque un artefact des anciens temps, tant **la voix est robotisée**. On n'y comprends pas grand chose et c'est **très moche** : 
 + 
 +<code> 
 +espeak -v fr -f text.txt 
 +</code> 
 + 
 +Concernant //svoxpico//, la voix est meilleure qu'//espeak//, ce qui n'est pas bien difficile au final. **Elle hoquette quelque peu mais cela lui confère un certain charme. Par contre elle est uniquement féminine** en langue française : 
 + 
 +<code> 
 +pico2wave -l fr-FR -w text.wav < text.txt && play text.wav 
 +</code> 
 + 
 +Avec //Mozilla TTS//, **les résultats sont plus probants**, même s'il y a quelques cafouillis par-ci par-là. Mais là encore, **seules des voix féminines sont disponibles** : 
 + 
 +<code> 
 +tts --text "$(cat text.txt)" --model_name tts_models/fr/mai/tacotron2-DDC --vocoder_name vocoder_models/universal/libri-tts/fullband-melgan && play tts_output.wav 
 +</code> 
 + 
 +**De fait, la solution que j'ai finalement retenue se sert d'//espeak// ! Mais en conjonction avec un autre logiciel, qui s'appelle //mbrola//.** Je l'ai installé, ainsi que l'ensemble des voix françaises, via cette commande : 
 + 
 +<code> 
 +sudo apt-get install mbrola mbrola-fr* 
 +</code> 
 + 
 +Cette fois-ci, **nous utilisons //espeak// pour générer un fichier de phonèmes, puis nous utilisons les vocoders de //mbrola// pour les lire** : 
 + 
 + 
 +<code> 
 +espeak -q -s 100 -v mb/mb-fr1 -f text.txt --pho --phonout=text.pho && mbrola /usr/share/mbrola/fr6/fr6 text.pho text.wav && play text.wav 
 +</code> 
 + 
 +Étrangement, cela ne marche pas avec l'argument //-v fr// d'//espeak//, il faut utiliser //-v mb/mb-fr1// à la place. 
 + 
 +**Pour changer de vocoder**, il faut **spécifier le chemin de celui-ci** dans la commande //mbrola//. Dans le cas précédent, c'est le chemin ///usr/share/mbrola/fr6/fr6// qui le spécifie. 
 + 
 +Cette fois-ci, nous avons **une voix masculine d'assez bonne qualité**, ce qui me satisfait. 
 + 
 +=====Quoi ma gueule ?===== 
 + 
 +Il faut maintenant associer la voix à un visage. Pour ce faire, **un petit tour sur le net et un copier-coller** d'un tableau représentatif de l'écrivain. 
 + 
 +Mais ce n'est pas tout, j'aimerai bien pouvoir **l'animer** un tantinet. Mon idée était de singer l'intelligence artificielle et la culture française **en transformant Jidéhéléf en pantin**. À l'aide de //[[https://www.gimp.org/|GIMP]]//, j'ai donc séparé le tableau original en plusieurs parties faciles à animer, **à la manière d'un système de calques** : 
 + 
 +{{ ::jdlfgimp.png?1000 |}} 
 + 
 +=====LaFontaine.exe===== 
 + 
 +Il est maintenant temps de regrouper ensemble les pensées, la voix et la frimousse de Jean ! 
 + 
 +J'avais en premier pensé utiliser le moteur de jeu vidéo libre //[[https://godotengine.org/|Godot]]// pour ce faire, car il intègre des outils d'animation simple à utiliser. Cependant, **j'ai finalement opté pour //[[https://tutos.labomedia.org/books/musique-assistee-par-ordinateur/page/quest-ce-que-supercollider|SuperCollider]]//** pour faire cela, notamment car c'est le logiciel le plus simple que je connaisse pour implémenter le déplacement de la mâchoire de Jean en fonction des conneries qu'il raconte. 
 + 
 +Voici le code du projet : 
 + 
 +<code> 
 +
 + 
 +var win = Window.new( 
 + "La Fontaine", 
 + Rect( 1000, 500, 200, 200 ) ); 
 + 
 +var filePath = thisProcess.nowExecutingPath.dirname ++ "/text.wav"; 
 + 
 +var lock = false; // Empêche de regénérer une fable tant que la précédente n'est pas finie 
 +var lockDelay = 1; // Délai supplémentaire de restriction 
 + 
 +var stringCmd; // Variable qui va stocker l'appel Bash à la génération du texte et du fichier son 
 + 
 +var view = UserView(); 
 + 
 +var imgJean = Image.new( 
 + thisProcess.nowExecutingPath.dirname ++ "/JEAN.png" ); 
 +var imgEmpty = Image.new( 
 + thisProcess.nowExecutingPath.dirname ++ "/emptyeyes.png" ); 
 +var imgChin = Image.new( 
 + thisProcess.nowExecutingPath.dirname ++ "/lafontaine_chin.png" ); 
 +var imgPupille = Image.new( 
 + thisProcess.nowExecutingPath.dirname ++ "/pupille.png" ); 
 + 
 +var textSpeed = 120; // Vitesse d'élocution 
 +var voiceTag = "fr3"; // Vocoder utilisé 
 + 
 +var jawOffset = 0; // Décalage de la mâchoire 
 + 
 +var eg = Point( 390, 512 ); // Position de l'oeil gauche 
 +var ed = Point( 385, 415 ); // Position de l'oeil droit 
 + 
 +var egtarget = Point( 390, 512 ); // Position de référence l'oeil gauche 
 +var edtarget = Point( 385, 415 ); // Position de référence l'oeil droit 
 + 
 +if( ~masterIn == nil, { // Bus master 
 + ~masterIn = Bus.audio( s, 1 ) } ); 
 + 
 +if( ~ampBus == nil, { // Bus de valeur d'amplitude 
 + ~ampBus = Bus.control( s, 1 ) } ); 
 + 
 + 
 +SynthDef( \master, { 
 + 
 + var sound = In.ar( ~masterIn, 1 ); 
 + var amp = Amplitude.kr( sound ); 
 + var panSound = Pan2.ar( sound ); 
 + 
 + Out.kr( ~ampBus, amp ); 
 + Out.ar( 0, panSound ); 
 + 
 +} ).play; 
 + 
 + 
 +// Construction de la commande Bash 
 +voiceTag = voiceTag ++ "/" ++ voiceTag; 
 + 
 +stringCmd = "cd ~/CNFS/bestiaireia/La_Fontaine/;"; 
 +stringCmd = stringCmd ++ "./mon_env/bin/python3 textgen.py > text.txt;"; 
 +stringCmd = stringCmd ++ "espeak -q -s " ++ textSpeed.asString ++ " -v mb/mb-fr1 -f text.txt --pho --phonout=text.pho;"; 
 +stringCmd = stringCmd ++ "mbrola /usr/share/mbrola/" ++ voiceTag ++ " text.pho text.wav"; 
 + 
 +// Fonction déclenchée à l'input 
 +win.view.keyDownAction = { arg view, char, modifiers, unicode,keycode; 
 + 
 + if( lock == false, { 
 + if( unicode == 32, { 
 + 
 + stringCmd.systemCmd; 
 + lock = true; 
 + 
 + b = Buffer.read( 
 + s, 
 + filePath, 
 + action: { { lock = false; }.defer( b.numFrames * ( 1.0 / b.sampleRate ) ) } 
 + ); 
 + 
 + SynthDef( "playbuf",
 + Out.ar( ~masterIn, 
 + PlayBuf.ar( 
 + numChannels: 1, 
 + bufnum: b.bufnum, 
 + rate: BufRateScale.kr( b.bufnum ) * 1, 
 + trigger: 1, 
 + startPos: 0, 
 + loop: 0, 
 + doneAction: Done.freeSelf ), 
 + 0.0 
 +
 + } ).play; 
 + } ); 
 + } ); 
 +}; 
 + 
 +win.view.layout_( HLayout() ); 
 + 
 +win.view.layout.add( view ); 
 + 
 +// Fonction d'affichage graphique : 
 +view.drawFunc = { 
 + Pen.drawImage( Point( 1920 - 1284 / 2 + 350, 1080 - 1000 / 2 + ( 1000 - 437 - 175 ) ), imgEmpty, operation: 'sourceOver', opacity:1); 
 + 
 + 
 + 
 + Pen.drawImage( Point( 1920 - 1284 / 2 + eg.x, 1080 - 1000 / 2 + eg.y ), imgPupille, operation: 'sourceOver', opacity:1); // Oeil droit 
 + 
 + Pen.drawImage( Point( 1920 - 1284 / 2 + ed.x, 1080 - 1000 / 2 + ed.y ), imgPupille, operation: 'sourceOver', opacity:1); // Oeil gauche 
 + 
 + Pen.drawImage( Point( 1920 - 1284 / 2, 1080 - 1000 / 2 ), imgJean, operation: 'sourceOver', opacity:1); 
 + Pen.drawImage( Point( 1920 / 2 - 132 + jawOffset, 1080 / 2 - 83 ), imgChin, operation: 'sourceOver', opacity:1); 
 +}; 
 + 
 + 
 +// Routine qui associe l'amplitude sonore à la position de la mâchoire : 
 +Routine( { 
 + 
 + var rand; 
 + var randN = 10; 
 + 
 + loop { 
 + 
 + ~ampBus.get( { | val | jawOffset = val.linlin( 0.0, 1.0, 0, 100 ); } ); 
 + jawOffset = jawOffset.asInteger; 
 + 
 + if( lock == true, { 
 + rand = randN.rand; 
 + if( rand == 0, { eg.x = eg.x + 1 } ); 
 + if( rand == 1, { eg.x = eg.x - 1 } ); 
 + if( eg.x < 385, { eg.x = 385 } ); 
 + if( eg.x > 410, { eg.x = 410 } ); 
 + 
 + rand = randN.rand; 
 + if( rand == 0, { eg.y = eg.y + 1 } ); 
 + if( rand == 1, { eg.y = eg.y - 1 } ); 
 + if( eg.y < 488, { eg.y = 488 } ); 
 + if( eg.y > 524, { eg.y = 524 } ); 
 + 
 + rand = randN.rand; 
 + if( rand == 0, { ed.x = ed.x + 1 } ); 
 + if( rand == 1, { ed.x = ed.x - 1 } ); 
 + if( ed.x < 380, { ed.x = 380 } ); 
 + if( ed.x > 395, { ed.x = 395 } ); 
 + 
 + rand = randN.rand; 
 + if( rand == 0, { ed.y = ed.y + 1 } ); 
 + if( rand == 1, { ed.y = ed.y - 1 } ); 
 + if( ed.y < 400, { ed.y = 400 } ); 
 + if( ed.y > 430, { ed.y = 430 } ); 
 + } ); 
 + 
 + if( lock == false, { 
 + if( eg.x < egtarget.x, { eg.x = eg.x + 1 } ); 
 + if( eg.x > egtarget.x, { eg.x = eg.x - 1 } ); 
 + if( eg.y < egtarget.y, { eg.y = eg.y + 1 } ); 
 + if( eg.y > egtarget.y, { eg.y = eg.y - 1 } ); 
 + 
 + if( ed.x < edtarget.x, { ed.x = ed.x + 1 } ); 
 + if( ed.x > edtarget.x, { ed.x = ed.x - 1 } ); 
 + if( ed.y < edtarget.y, { ed.y = ed.y + 1 } ); 
 + if( ed.y > edtarget.y, { ed.y = ed.y - 1 } ); 
 + } ); 
 + 
 + { view.refresh }.defer; 
 + 
 + (1/30).wait 
 + }; 
 +} ).play; 
 + 
 +win.background = Color.black; // Set main widow background as black 
 +win.fullScreen; 
 + 
 +win.front; 
 + 
 +// À la fermeture, libération de la mémoire : 
 +CmdPeriod.doOnce( { 
 + Window.closeAll; 
 + s.freeAll; 
 + imgChin.free; 
 + imgJean.free; 
 + imgEmpty.free; 
 + imgPupille.free; 
 +} ); 
 + 
 +
 +</code> 
 + 
 +Sans rentrer trop dans le détail, quelques points sont intéressants à aborder sur l'algorithme. 
 + 
 +===Corrélation de l'audio et de l'affichage graphique=== 
 + 
 +L'idée principale était d'**utiliser SuperCollider afin de relier le volume sonore ( l'amplitude ) du fichier son au déplacement de la mâchoire**. Pour mettre cela en place, j'ai d'abord décidé d'utiliser //espeak// et //mbrola// de telle manière qu'ils produisent non pas un son direct, mais **un enregistrement audio**. 
 + 
 +Au lancement du programme, **un synthé maître est créé**, qui utilise un //Ugen IN// afin de pouvoir bénéficier d'un son d'entrée créé //a posteriori//. Lors de la génération d'une fable, **le fichier son résultant est chargé dans SC**, puis **envoyé dans l'entrée du synthé maître**. **Important** : le synthé créé lors de la génération est supprimé lors de la fin de la lecture grâce au paramètre //doneAction: Done.freeSelf// du //PlayBuf//. Le cas échéant, les //UGens// créés pour jouer le sample resteraient présent en mémoire et risqueraient de rapidement saturer l'ordinateur. 
 + 
 +Dans le synthé maître, avant de **le passer en stéréo et de l'envoyer vers la carte son**, **le son est en premier lieu analysé grâce à l'//UGen Amplitude//, qui redirige son résultat d'analyse dans un //Bus//** créé au préalable. 
 + 
 +C'est **une //Routine// qui**, à intervalle régulier, **récupère la valeur d'amplitude au sein du //Bus//**, modifie la valeur de référence de la position de la mâchoire puis lance un appel de rafraîchissement de l'interface graphique. On utilise //{}.defer// car **la mise-à-jour graphique ne peut s'effectuer dans le contexte audio**. 
 + 
 +La méthode d'accès à une valeur présente dans un //UGen//, c'est-à-dire une information qui passe du serveur à //sclang//, [[https://scsynth.org/t/getting-values-out-of-ugens/489|est discutée dans ce post sur le forum de SC]], et [[http://modularbrains.net/dx490a/DX490A_su2010_02.1_%5BServer-language_communication%5D.html|approfondie dans ce tutoriel]]. 
 + 
 +===Commandes BASH depuis SC et synchronicité=== 
 + 
 +**Sous //Linux//, il est très simple d'envoyer des commandes //BASH// dans le terminal depuis //SC//.** Pour ce faire, **on utilise une méthode sur une chaîne de caractères qui contient la commande à exécuter.** Pour exécuter plusieurs commandes, on les sépare par des points virgules : 
 + 
 +<code> 
 +"ls".unixCmd 
 +</code> 
 + 
 + 
 +<code> 
 +"cd Dossier/;ls".systemCmd 
 +</code> 
 + 
 +Dans //SuperCollider//, on distingue **deux types de fonctions : les commandes //synchrones//, et les commandes //asynchrones//**. En règle générale, **une commande synchrone bloque l'éxecution du code dans //sclang// le temps que le serveur se mette à jour.** Cela permet par exemple d'éviter, après avoir ajouté une //SynthDef//, de l'appeler avant qu'il soit effectivement initialisé. 
 + 
 +Dans notre cadre, la génération de la fable, et dans une moindre mesure la génération du fichier audio, prend un peu de temps à l'ordinateur. **Nous utilisons donc .//systemCmd//, qui est la fonction synchrone d'accès au terminal.** 
 + 
 +Le cas échéant, en utilisant .//unixCmd//, le fichier audio est chargé par SuperCollider avant que la nouvelle fable soit générée, c'est donc la fable précédente qui est récitée... 
 + 
 +Ici, un problème posé est que la suspension d'activité dans //sclang// via **l'appel à la fonction synchrone .//systemCmd// arrête également la //Routine//** responsable de la mise-à-jour graphique, **ce qui bloque l'animation**. J'ai résolu ce problème en n'animant pas la partie d'attente au cours de laquelle aucune fable n'est récitée. Le blocage de la //Routine// est donc invisible à l'utilisateur, même s'il existe. 
 + 
 +Dans l'absolu, il aurait fallu plutôt utiliser .//unixCmd//, et mettre en place un retour depuis le terminal afin de lancer l'enregistrement audio après la génération du nouveau fichier. Tant qu'on y est, il est à noter que lorsque l'image est immobile, la //Routine// contiue de tourner, ce qui n'est pas optimal mais pas catastrophique par ailleurs. 
 + 
 +=====La Suite ?===== 
 + 
 +À ce stade, **le prototype est fonctionnel mais quelques améliorations sont possibles** : 
 + 
 +  * Refaire l'algorithme de Routine pour qu'il ne reste activé que le temps de l'animation. 
 +  * Mettre l'image de Jean de La Fontaine en plein écran, mais cela dépend de la résolution du moniteur que nous utiliserons pour l'installation. 
 +  * Ajouter un retour visuel de la fable générée, parce que la voix n'est pas toujours claire, et pour les publics malentendants. 
 +  * Réaliser le dispositif physique de l'installation, avec un joli cadre en bois, et tout le toutim. 
 +  * Trouver un moyen de générer des fables plus longues. 
 + 
 +{{tag> bestiaire_ia tal }}
numeriser_jean_de_la_fontaine.1646407463.txt.gz · Dernière modification : 2022/03/04 15:24 de Simon Deplat