• contact@spgoo.org

PASSY-DPFV — resp. Gabriel Bergounioux

Construction d’une plateforme nommée Passy sur la base de la réalisation effectuée pour MP Tours (DeepBdd-Bddictionnairique) dans un contexte de langue française. Ce travail est réalisé en collaboration avec G. Bergounioux et avec la participation de l’ATILF (Nancy).

Première étape: récupération des données du TLF de l’ATILF pour constituer la base de départ de notre corpus phonétique soit 54734 entrées.

Elaboration d’un workflow de traitements pour permettre à la plateforme d’avoir un côté communautaire, on envisage dès le départ une saisie multiple par des utilisateurs de type contributeur. Ce qui permettra d’avoir, vu le chantier, des forces vives pour alimenter et corriger les entrées. Dans ce circuit d’alimentation deux voies sont prévues : une alimentation manuelle et une alimentation automatique.

Alimentation manuelle : réservée aux contributeurs, elle permettra à des utilisateurs spécifiques de proposer des corrections et ajouts des données à partir d’une interface dédiée. Un environnement

Alimentation automatique : déclenchée par le Backend de la plateforme pour l’extraction de corpus sonores. cet agent IA devra en permanence sonder des corpus existants et des fonds sonores publics pour en extraire des séquences et les proposer à la plateforme.

Pour ces deux circuits d’alimentation nous mettons en place un Workflow qui aura pour objectif de valider les propositions des contributeurs et de la partie IA afin de garantir une grande qualité des données mises à la disposition des utilisateurs.

Constitution et extraction des entrées fournies par ESLO2 : le corpus nous permet d’extraire à partir des transcriptions C des enregistrements des illustrations sonores très variées des mots et de leur prononciation.

Création d’un script python pour extraire des fichiers de transcriptions les différentes informations.

Préparation de l’environnement pour les extractions: Installation des packages suivants pour faciliter et interpréter les données extraites dans un environnement python3.12

pip3 install spacy nlp nltk 
python -m spacy download fr_core_news.sm 
pip install git+https://github.com/ClaudeCoulombe/FrenchLeffLemmatizer.git

Ces différents package vont nous permettre d’obtenir directement la lemmatisation des mots pour constituer un lexique et la morpho-syntaxe des expressions extraites le tout enregistrer dans deux collections de notre base de données MongoDB : eslo2 et lexique_eslo2

Retour expérience du parser XML : import xml.etree.ElementTree as ET

1 / Ce package entre les versions 3.9 et 3.12 de python a évolué et l’emploi de getChildren pour trouver les enfants d’une branche de la structure XML n’est plus accessible dans les dernières versions.

2 / Interprétation des blocs à l’aide du parser.

Bloc classique :

<Turn speaker="spk1" startTime="403.994" endTime="407.699">
<Sync time="403.994"/>
oui mais je me suis excusé c'est bon là
<Sync time="406.048"/>
j'ai pas fait exprès
</Turn>

Bloc spécifique :

<Turn startTime="0" endTime="120.852" speaker="spk1">
<Sync time="0"/>

<Sync time="0.586"/>

<Event desc="rire" type="noise" extent="instantaneous"/>

<Sync time="1.409"/>
c'est moi
<Sync time="2.462"/>

<Sync time="120.295"/>
ça va ?
</Turn>

Attention aux lignes vides, lors de la recomposition du bloc, elles ne sont pas interprétées par le parser. Si on reconstitue ce bloc de la manière suivante ….<Sync time=”0″/><Sync time=”0.586″/><Event desc=”rire” type=”noise” extent=”instantaneous”/>…
le parser ne distingue pas les éléments intermédiaires et repositionne mal les groupes de mots. D’où l’ajout systématique d’une chaîne de caractères que l’on pourra distinguer et filtrer par la suite ….<Sync time=”0″/>YS<Sync time=”0.586″/>YS

Nous faisons le choix dans le traitement des transcriptions de ne pas prendre en compte les blocs multi-speakers pour éviter de la confusion et afin d’avoir des enregistrements clairs et précis.

Exemple de bloc multi-speakers

<Turn speaker="spk5 spk1" startTime="407.699" endTime="410.971">
<Sync time="407.699"/>
<Who nb="1"/>
bon tu es tu es
<Event desc="pi" type="pronounce" extent="instantaneous"/>
 parce que c'est
<Event desc="rire" type="noise" extent="instantaneous"/>

<Who nb="2"/>

<Event desc="rire" type="noise" extent="instantaneous"/>

<Sync time="410.086"/>
<Who nb="1"/>

<Who nb="2"/>

</Turn>

On peut constater que dans ce découpage les éléments ne sont pas assez précis et n’apportent pas d’informations supplémentaires pour notre plateforme passy, qui met en avant l’écoute et la prononciation des mots par un seul locuteur.

Détail du code pour l’extraction
def traite_bloc(bb,speakers,fichier):
    """
    Traitement du bloc Turn pour en extraire les éléments 
    """
    global nb_speakers_multiple, liste_mots
    tree = ET.ElementTree(ET.fromstring(bb))
    root=tree.getroot()
    Turn=root.attrib
    # on regarde si plusieurs speakers 
    if "speaker" not in Turn:
        print("pas de speaker dans le bloc")
        return
    tab_speakers=Turn["speaker"].split(" ")
    D_bloc_recompose={}
    if len(tab_speakers)<2 :
        txt_complet=""
        textes=[]
        debut=Turn["startTime"]
        ref_end=Turn["endTime"]
        #on parcourt les deux listes en même temps
        for indice,txt in zip(list(root), root.itertext()):
            if "time" in indice.attrib :  #<Sync time="407.699"/>
                ref_debut= indice.attrib['time']
                if debut!=ref_debut:
                    textes.append({"debut":debut, "fin":ref_debut,"texte":txt_complet.strip()})
                    debut=ref_debut
                txt_complet=txt
        #pour le dernier 
        textes.append({"debut":debut, "fin":ref_end,"texte":txt_complet.strip()})
        for texte in textes:
            D_bloc_recompose={}
            if texte["texte"]!="" and texte["texte"]!="YS" :
                mot_propre=texte["texte"].replace("'"," ").replace("?"," ")
                tab_res = re.split('\s+',  mot_propre)
                for mot in tab_res:
                    if mot in liste_mots:
                        liste_mots[mot]+=1
                    else:
                        liste_mots[mot]=1
                D_bloc_recompose["enreg"]=fichier
                D_bloc_recompose["locuteur"]=speakers[Turn["speaker"]]["name"]
                D_bloc_recompose["texte"]=texte["texte"]
                D_bloc_recompose["morpho"]=[{y:x} for x,y in return_POS(texte["texte"])]
                D_bloc_recompose["debut"]=texte["debut"]
                D_bloc_recompose["fin"]=texte["fin"]
                # on enregistre la structure directement dans MongoDB
                mycol.insert_one(D_bloc_recompose)
    else :
        nb_speakers_multiple+=1

Pour ajouter la possibilité d’écouter les éléments sonores avant et après un segment il est nécessaire de disposer de la totalité des segments pour tous les enregistrements ENTRETIEN d’ESLO. Ensuite de réadapter le web service au niveau de Spring Boot pour permettre cette récupération. Pour ce faire on a implémenté de nouvelles entrées dans l’API get_avant_ et get_apres qui s’appuient sur le type de requête ci-dessous :

db.getCollection(‘eslo2’).aggregate([{“$addFields”:{nouv:{$toDouble:”$debut”}}},{$match:{$and:[{$expr:{$gt:[“$nouv”,280.261]}},{$expr:{$lt:[“$nouv”,301.261]}},{“enreg”:”ESLO2_ENT_1010_C”}]}}])

On génère un nouveau champ nouv pour transformer la donnée texte en donnée numérique afin de pouvoir la comparer.

// chercher les segments avant et après celui courant
// on va prendre le debut -10 et la fin + 10 
@RequestMapping(value = "/get_avant_", method = POST)
@PostMapping("json")
public List<Proposition_IA> get_avant_(@RequestBody Map<String, String> body) {
	String locuteur = body.get("locuteur");
	double debut= Double.parseDouble(body.get("debut"));
	double fin=  Double.parseDouble(body.get("fin"));
	String enreg=  body.get("enreg");			
	AddFieldsOperation Addfield = Aggregation.addFields().addField("nouv").withValue(ConvertOperators.ToDouble.toDouble("$debut")).build();
	// avant 
	Criteria critres = new Criteria().andOperator(
		Criteria.where("nouv").lt(debut),
		Criteria.where("nouv").gt(debut-10),
		Criteria.where("enreg").regex(enreg));
	MatchOperation matchStage = Aggregation.match(critres);
	Aggregation aggregation = Aggregation.newAggregation(Addfield, matchStage);
	AggregationResults<Proposition_IA> output_avant = mongoTemplate.aggregate(aggregation, "eslo2", Proposition_IA.class);
	List<Proposition_IA> avant_result = output_avant.getMappedResults();
	return avant_result;
}	

Description de la classe : propositions_IA

private String _id;
private String locuteur;
private String  enreg ;
private String  texte ;
private ArrayList<String>  morpho ;
private double debut ;
private double fin ;
private Statut statut ;

La partie Morpho a été neutralisée dans le visuel pour le moment. L’ensemble des résultats sont ensuite descendus au niveau du navigateur dans l’interface WEB et positionnés dans une div autour du bouton lecture. Ce qui permet à l’utilisateur de charger uniquement les éléments nécessaires à la compréhension du segment lors de l’analyse.

Implémentation de la prise de note au niveau des entrées : on souhaite également conserver les informations associées au segment, pour ce faire on a aménagé un système de prise de note qui permet d’accompagner les chercheurs dans leur exploration et lors de leur sélection et ensuite de les partager entre eux. L’accès en modification est autorisé au seul propriétaire de la note. Au niveau du web service il a fallu mettre en place un système de gestion de ces notes dans deux contextes: contexte lors du travail préparatoire BROUILLON et lors de la sélection des entrées VALID, ce qui correspond à l’onglet Corpus ESLO2 et Exemplaires de l’interface.

Une collection Annotation dédiée a été générée pour conserver ce type d’information et est reliée par l’ID à l’entrée correspondante.


La plateforme PASSY doit permettre aux utilisateurs de travailler dans les meilleures conditions et d’offrir les fonctionnalités nécessaires pour les différentes phases. On a identifié plusieurs phases :

  • phase d’alimentation qui se concentre principalement au niveau du module de supervision et qui est dédiée aux expert(e)s ,
  • phase d’exploitation/extraction qui est dédiée à toue les utilisateurs de la plateforme

Pour ces deux phases il semble intéressant d’offrir des outils d’analyse de signaux sonores sur un ou plusieurs éléments afin d’avoir des mécanismes de comparaison et d’extraction qui reposent sur des métriques associés directement au signal que l’on pourra croiser avec les méta-données.

Praat est disponible en version desktop sur les environnements Windows, Linux et Mac. Il offre également des possibilités de scripts qui permettent d’encapsuler des traitements en lots sur des sources définies. De plus une des versions de Praat est disponible en mode commande. Exemples de scripts https://github.com/FieldDB/Praat-Scripts

Donc le dispositif que l’on envisage avec ce type de solution est le suivant :

  • Définir les fonctionnalités que l’on souhaite offrir lors des différentes phases
    • Phase de supervision — pour les expert(e)s
    • Phase d’exploitation — pour les utilisateurs
  • Traduire ces fonctionnalités en scripts
  • Tester les scripts pour la production de résultats sur un ou plusieurs éléments sonores
  • Tester l’intégration et l’exécution de ces scripts dans un environnement en mode commandes (sans interface graphique )
  • Intégrer dans un web service le dispositif d’appel et de restitution des résultats de traitements à la plateforme Passy

Sous python : voir le site de https://parselmouth.readthedocs.io/en/stable/
Installation sous linux : pip install praat-parselmouth

Exemple de mise en oeuvre avec une extraction simple

import parselmouth

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
Matplotlib is building the font cache; this may take a moment.
sns.set() # Use seaborn's default style to make attractive graphs
plt.rcParams['figure.dpi'] = 100 # Show nicely large images in this notebook
snd = parselmouth.Sound("audio/the_north_wind_and_the_sun.wav")
plt.figure()
plt.plot(snd.xs(), snd.values.T)
plt.xlim([snd.xmin, snd.xmax])
plt.xlabel("time [s]")
plt.ylabel("amplitude")
plt.show() # or plt.savefig("sound.png"), or plt.savefig("sound.pdf")

Pour extraire une partie du signal et faire une représentation plus fine d’une

Utilisation de cette solution sous forme de web service via flask voir

Détail de la bibliothèque : parselmouth

parselmouth.AmplitudeScaling( ) parselmouth.PitchUnit()parselmouth.SoundFileFormat()
parselmouth.CC(  )  parselmouth.praat ()parselmouth.SpectralAnalysisWindowShape()
parselmouth.Data() parselmouth.PraatError() parselmouth.Spectrogram()
parselmouth.Formant()parselmouth.PraatFatal() parselmouth.Spectrum()
parselmouth.FormantUnit(parselmouth.PRAAT_VERSION parselmouth.TextGrid()
parselmouth.Function(  )parselmouth.PRAAT_VERSION_DATEparselmouth.Thing(
parselmouth.Intensity(   ) parselmouth.read( )parselmouth.TimeFunction(
parselmouth.Interpolation()parselmouth.Sampled(  ) parselmouth.ValueInterpolation()
parselmouth.Matrix()parselmouth.SampledXY() parselmouth.Vector()
parselmouth.MFCC( )parselmouth.SignalOutsideTimeDomainparselmouth.VERSION
parselmouth.Pitch()parselmouth.Sound( )parselmouth.WindowShape()

Fonctions associées à parselmouth.Sound()

Code exemple pour le web service et intégration dans notre contexte

%%writefile server.py

from flask import Flask, request, jsonify
import tempfile

app = Flask(__name__)

@app.route('/pitch_track', methods=['POST'])
def pitch_track():
    import parselmouth

    # Save the file that was sent, and read it into a parselmouth.Sound
    with tempfile.NamedTemporaryFile() as tmp:
        tmp.write(request.files['audio'].read())
        sound = parselmouth.Sound(tmp.name)

    # Calculate the pitch track with Parselmouth
    pitch_track = sound.to_pitch().selected_array['frequency']

    # Convert the NumPy array into a list, then encode as JSON to send back
    return jsonify(list(pitch_track))