Erreur d'execution SFSpeechRecognizer et AVSpeechSynthesizer

Bonjour j’essaie de créer un exemple avec swiftUI avec les frameworks SFSpeechRecognizer et AVSpeechSynthesizer mon exemple fonctionne sur le simulateur, mais pas sur un iPhone.

J’arrive bien à faire plusieurs enregistrements de ma voix à la suite les uns des autres, idem pour la lecture. L’application crache si je veux refaire un enregistrement après avoir fait une lecture.

je ne sais pas interpréter le message erreur de la console

il me manque des choses dans mon code ?

import Speech
import AVFoundation

class ReconnaissanceVocaleViewModel:NSObject, ObservableObject, SFSpeechRecognizerDelegate {
    // création du manageur + configuration de la langue
    //var speechRecognizer:SFSpeechRecognizer? = SFSpeechRecognizer(locale: Locale(identifier: "fr-FR"))
    // création du manageur + configuration de la langue
    var speechRecognizer:SFSpeechRecognizer? = SFSpeechRecognizer(locale: Locale(identifier: Locale.current.identifier))
    // Gestion du partage voix
    let audioSession = AVAudioSession.sharedInstance()
    // moteur
    let engine = AVAudioEngine()
    // requete
    var request: SFSpeechAudioBufferRecognitionRequest?
    // tache de transcription de la voix en texte
    var task: SFSpeechRecognitionTask?
    
    
    @Published var enregistrementEnCours:Bool = false
    @Published var transformerVoixText:String?
    @Published var boutonUtilisationMicro:Bool = false
    
    override init() {
        super.init()
        speechRecognizer?.delegate = self
        
        // autorisation utilisation micro
        SFSpeechRecognizer.requestAuthorization { (autorisationMicro) in
            OperationQueue.main.addOperation { [self] in
                switch autorisationMicro {
                case .authorized:
                    print("permission accordé")
                    boutonUtilisationMicro = true
                case .notDetermined:
                    print("aucune réponse")
                case .denied:
                    print("aucune permission accordé")
                    boutonUtilisationMicro = false
                case .restricted:
                    print("seulement si l'application est active")
                    boutonUtilisationMicro = true
                default:
                    print("réponse par défault")
                }
            }
        }
    }
    // demarre la transcription de la voix en texte
    func demarrerTranscriptionVoix() {
        //supprime les transcription precédentes
        enregistrementEnCours = true
        if task != nil {
            task?.cancel()
            task = nil
        }
        
        //Préparer l'enregistrement
        // Le nœud audio pour l'entrée singleton du moteur audio. (doc apple)
        let node = engine.inputNode
        
        //configuration de la session
        do {
            try audioSession.setCategory(.record, mode: .measurement, options: .mixWithOthers)
            try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
            request = SFSpeechAudioBufferRecognitionRequest()
            guard request != nil else { return }
            task = speechRecognizer?.recognitionTask(with: request!, resultHandler: { (resultat, erreur) in
                //Erreur ou resultat final
                if erreur != nil || (resultat != nil && resultat!.isFinal) {
                    //Arrête
                    print(erreur?.localizedDescription ?? "")
                    // arret du moteur, arret du processus
                    self.engine.stop()
                    
                    // suppression bus enregistrement
                    node.removeTap(onBus: 0)
                    self.request = nil
                    self.task = nil
                }
                
                if resultat != nil {
                    self.transformerVoixText = resultat!.bestTranscription.formattedString
                    //print(self.transformerVoixText) // affiche texte dans la console
                    
                }
            })
            let format = node.outputFormat(forBus: 0)
            node.installTap(onBus: 0, bufferSize: 1024, format: format) { (buffer, time) in
                self.request?.append(buffer)
            }
            engine.prepare()
            do {
                try engine.start()
            } catch {
                print("Erreur au lancement => \(error.localizedDescription)")
            }
        } catch {
            print("Erreur du set de catégory => \(error.localizedDescription)")
        }
    }
    // arret de la retranscrpition de la voix
    func arretTranscription() {
        do {
            engine.stop()
            request?.endAudio()
           try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
            enregistrementEnCours = false
        } catch {
            print(error.localizedDescription)
            
        }
       
    }
import AVFAudio
import AVFoundation

class SyntheseVocaleViewModel:NSObject, ObservableObject, AVSpeechSynthesizerDelegate {
    
    var speechSynthesizer:AVSpeechSynthesizer = AVSpeechSynthesizer()
    // variable de partage du canal audio
    var audioSession = AVAudioSession.sharedInstance()
    
    
    // vitesse de lecture
    var rate:Float = AVSpeechUtteranceDefaultSpeechRate
    
    var volume:Float = 0.5
    
    // configuration type de langue
    var voice = AVSpeechSynthesisVoice(identifier: Locale.current.identifier)
    
    // variable etat de la lecture
    @Published var lectureEnCours:Bool = false
    
    override init() {
        super.init()
        self.speechSynthesizer.delegate = self
        
    }
    // volume Audio
    func volumeAudio(niveauVolume:CGFloat) {
        volume = Float(niveauVolume)
    }
    
    // definir la vitesse de lecture
    func rythmeLecture(vitesseLecture:CGFloat) {
        rate = Float(vitesseLecture)
    }

    // lecture du texte
    func demarrerLecture(texte:String) {
        // gestion du mode lecture
        // gestion de partage du canal de son entre plusieurs application.
        do {
            // configuration du type de flux audio
            try audioSession.setCategory(.playAndRecord, mode: .spokenAudio, options: .defaultToSpeaker)
            // demande l'utilisattion  du canal audio
            try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
            // utterance (contient le texte à lire.
            let utterance = AVSpeechUtterance(string: texte)
            utterance.voice = voice
            //utterance.volume = volume
            utterance.rate = rate
            speechSynthesizer.speak(utterance)
            
            
        } catch let error as NSError {
            print("type erreur \(error.localizedDescription)")
        }
    }
    
    // arrete la lecture
    func arretLecture() {
        // arret de la lecture après le dernier mot en cours
        speechSynthesizer.stopSpeaking(at: .word)
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
        print("Début de lecture")
        self.lectureEnCours = true
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
        // non utilisé pour l'exemple
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) {
        // non utilisé pour l'exemple
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
        // non utilisé pour l'exemple
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
        // non utilisé pour l'exemple
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        do {
            // rend le flux audio disponible pour les autre application
            try self.audioSession.setActive(false, options: .notifyOthersOnDeactivation)
            print("Terminer fin de lecture.")
            self.lectureEnCours = false
            
        } catch let error as NSError {
            print("type erreur \(error.localizedDescription)")
        }
    }
}

pour mieux comprendre
une vidéo exemple https://youtu.be/CHJwLVca0rU

L’exemple complet sur code GitHub

Merci de votre aide.

Hello, ton lldb est plutôt explicite je trouve, il te donne la raison (tu ne remplie pas une condition) un peu partout :

Bonsoir, merci de ta réponse, mais ce que je ne comprends pas, dans SFSpeechRecognizer je gère bien la création de mon bus et la suppression une fois l’arrêt de l’enregistrement.

if erreur != nil || (resultat != nil && resultat!.isFinal) {
                    //Arrête
                    print(erreur?.localizedDescription ?? "")
                    // arret du moteur, arret du processus
                    self.engine.stop()
                    
                    // suppression bus enregistrement
                    node.removeTap(onBus: 0)
                    self.request = nil
                    self.task = nil
                }

Par contre du côté lecture (AVSpeechSynthesizer) je ne gère pas toute cette partie, et après la lecture j’aurais un bus audio de créer onbus: 0 que je ne supprime pas ?.

Je dis cela, car c’est le basculement de la lecture vers un nouvel enregistrement speech qui provoque l’erreur.

Les docs apple ne sont pas claire pour moi donc je vais encore regarder.
Autre question que je me pause, pourquoi dans le simulateur le basculement se passe bien ?

cherche avec un ton lldb et un breackpoint plutot que d’attendre la levée de l’exception :stuck_out_tongue:

Tu pourras vérifer que ton code ce comporte comme prévue a toutes les étapes.

cherche avec un ton lldb et un breackpoint plutôt que d’attendre la levée de l’exception

Low Level Debugger LOL.Sauf que la je veux bien, mais je ne sais même pas ou mettre le point d’ arrêt, car je ne suis même pas sur que le code écrit est conforme. c’est pas mon métier, Lol je mesure tout le chemin qui me reste à parcourir ! Je bosse demain on verra celà après.

Merci toi pour les conseils

Je pense qu’a demarrerTranscriptinVoix est un bon debut :slight_smile:

Bonsoir je rentre du boulot, juste pour rappel j’ai refais des tests hier,
la transcription de voix fonctionne bien, je peux même en faire plusieurs à la suite sans problème (framework Speech) → « SFSpeechRecognizer » ma voix est bien capturé et transformé en texte.

l’autre module marche bien également le texte est bien lu avec la voix de synthèse autant de fois que je le souhaite.

le crache se produit quand je souhaite refaire une transcription de voix après avoir lu avec la voix de synthèse. et uniquement sur un vrai device.

je confirme tout celà avec le lldb et je reviens vous voir

Breack à ce moment, puis tu y vas step by step et analyse ton code et vérifie avec « po » t es variables / conditions afin de contrôler que ton code fonctionne comme prévue, tu devrais faire des tests unitaires , ça te permettrais de ne pas régresser lors de t es implémentations :stuck_out_tongue:

Info : un breackpoint peut etre placé à chaud pendant l’execution de l’app

Je pense avoir résolu mon erreur, il faut que je comprenne complètement pourquoi ? et oui ?
coté lecture voix de synthèse "SyntheseVocaleViewModel’

 try audioSession.setCategory(.playAndRecord, mode: .spokenAudio, options: .mixWithOthers)

MAJ du gitHub
effectivement lldb aide un peu :sweat_smile:

Lldb est notre seigneur à tous :pray:

Ah non ! Mon seigneur c’est St Jobs !

1 « J'aime »

Bonsoir j’ ai besoin d’une piste de réflexion sur la méthode à mettre en oeuvre,

import Speech
import AVFoundation

extension Notification.Name {
    static let tacheDeRetrabscription = Notification.Name("ajoutTacheNotification")
}

class ReconnaissanceVocaleViewModel:NSObject, ObservableObject, SFSpeechRecognizerDelegate {
    // création du manageur + configuration de la langue
    //var speechRecognizer:SFSpeechRecognizer? = SFSpeechRecognizer(locale: Locale(identifier: "fr-FR"))
    // création du manageur + configuration de la langue
    var speechRecognizer:SFSpeechRecognizer? = SFSpeechRecognizer(locale: Locale(identifier: Locale.current.identifier))
    // Gestion du partage voix
    let audioSession = AVAudioSession.sharedInstance()
    // moteur
    let engine = AVAudioEngine()
    // requete
    var request: SFSpeechAudioBufferRecognitionRequest?
    // tache de transcription de la voix en texte
    var task: SFSpeechRecognitionTask?
    
    
    @Published var enregistrementEnCours:Bool = false
    @Published var transformerVoixText:String?
    @Published var boutonUtilisationMicro:Bool = false
    
    override init() {
        super.init()
        speechRecognizer?.delegate = self
        
        // autorisation utilisation micro
        SFSpeechRecognizer.requestAuthorization { (autorisationMicro) in
            OperationQueue.main.addOperation { [self] in
                switch autorisationMicro {
                case .authorized:
                    print("permission accordé")
                    boutonUtilisationMicro = true
                case .notDetermined:
                    print("aucune réponse")
                case .denied:
                    print("aucune permission accordé")
                    boutonUtilisationMicro = false
                case .restricted:
                    print("seulement si l'application est active")
                    boutonUtilisationMicro = true
                default:
                    print("réponse par défault")
                }
            }
        }
    }
    // demarre la transcription de la voix en texte
    func demarrerTranscriptionVoix() {
        //supprime les transcription precédentes
        enregistrementEnCours = true
        if task != nil {
            task?.cancel()
            task = nil
        }
        
        //Préparer l'enregistrement
        // Le nœud audio pour l'entrée singleton du moteur audio. (doc apple)
        let node = engine.inputNode
        
        //configuration de la session
        do {
            try audioSession.setCategory(.record, mode: .measurement, options: .mixWithOthers)
            try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
            request = SFSpeechAudioBufferRecognitionRequest()
            guard request != nil else { return }
            task = speechRecognizer?.recognitionTask(with: request!, resultHandler: { (resultat, erreur) in
                //Erreur ou resultat final
                if erreur != nil || (resultat != nil && resultat!.isFinal) {
                    //Arrête
                    print(erreur?.localizedDescription ?? "")
                    // arret du moteur, arret du processus
                    self.engine.stop()
                    
                    // suppression bus enregistrement
                    node.removeTap(onBus: 0)
                    self.request = nil
                    self.task = nil
                }
                
                if resultat != nil {
                    self.transformerVoixText = resultat!.bestTranscription.formattedString
                    NotificationCenter.default.post(name:               NSNotification.Name.tacheDeRetrabscription, object: self.transformerVoixText)
                    //print(self.transformerVoixText) // affiche texte dans la console
                    
                }
            })
            let format = node.outputFormat(forBus: 0)
            node.installTap(onBus: 0, bufferSize: 1024, format: format) { (buffer, time) in
                self.request?.append(buffer)
            }
            engine.prepare()
            do {
                try engine.start()
            } catch {
                print("Erreur au lancement => \(error.localizedDescription)")
            }
        } catch {
            print("Erreur du set de catégory => \(error.localizedDescription)")
        }
    }
    // arret de la retranscrpition de la voix
    func arretTranscription() {
        do {
            engine.stop()
            request?.endAudio()
           try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
            enregistrementEnCours = false
        } catch {
            print(error.localizedDescription)
            
        }
       
    }
@StateObject var utiliserMicro:ReconnaissanceVocaleViewModel = ReconnaissanceVocaleViewModel()
@State var monTexte:String = ""
var body: some View {
TextEditor(text:  $monTexte)
     .frame(width: 285, height: 350)
}

Button {
                        // Action
                        voixSynthese.arretLecture()
                        utiliserMicro.etatProcessus()
                        
                    } label: {Text("Enregistrer ....") }

Une fois lancer l’enregistrement la variable @Published var transformerVoixText:String? mette à jour en temps réel le binding texte de la vue TextEditor de la contentView pour le moment j’utilise une solution « KK » qui ne me convient pas du tout, car le texte n’est pas modifiable. Et de plus je perds les propriétés de TextEditor.

TextEditor(text:  .constant(utiliserMicro.transformerVoixText ?? ""))

besoin de piste ? Merci beaucoup

Bonsoir les codeurs je viens de résoudre mon problème avec les notifications j’avais jamais utilisé en swiftUI

dans ma class ReconnaissanceVocaleViewModel:NSObject, j’ai

extension Notification.Name {
    static let tacheDeRetrabscription = Notification.Name("ajoutTacheNotification")
}

NotificationCenter.default.post(name: NSNotification.Name.tacheDeRetrabscription, object: self.transformerVoixText)

et dans le contentView mon textEditor resemble à ceci

TextEditor(text:  $TexteRetranscrit)
                        .onReceive(NotificationCenter.default.publisher(for: Notification.Name.tacheDeRetrabscription), perform: {
                            TexteRetranscrit = $0.object as? String ?? ""
                        })

Bonne soirée et merci pour votre aide

Oui les notifications étaient très utiles en MVC avec UIKit , je trouve que cela a moins d’intérêt avec le MVVM de SwiftUI.

Belle journée à toi,

Bonjour, Nono92,

Grâce à cette technique, je peux mettre à jour le binding de ma vue TextEditor en temps réel à chaque changement de la méthode demarrerTranscriptionVoix() « lorsque je parle » je n’ai pas trouvé une autre manière de procéder.

S’il y a une autre manière de faire je veux bien m’y intéresser, si d"autres personnes veulent utiliser cet exemple pour leur application aucun problème.

GitHub parleMOI

Bonne journée à toi