“Faut qu’on parle…”

hello computer

  1. Dans la première partie nous avons vu les bases de l’utilisation des MethodChannels.

Nous allons maintenant regarder plus en détail une implémentation de base de la reconnaissance vocale.

Speech recognition

Dans Sytody, l’UI fontionne de la manière suivante :

  • une fois la reconnaisance vocale détectée, un bouton permet de lancer l’enregistrement
  • quand l’enregistrement démarre,
    • le bouton d’écoute se transforme en bouton ‘Stop’, permettant de finaliser l’écoute et la transcription.
    • le champ de trancription apparait, si il y a des stranscription intermédiaires, elles sont affichées, et un bouton [x]permet d’annuler l’écoute et donc la transcription en cours.

Fonctionnement de Sytody

API natives

Android(4.1+) et iOS(10+) proposent chacun une API de reconnaissance vocale :

Pour les utiliser depuis l’application Flutter, nous allons donc définir un canal dédié aux fonctionnalités de reconnaissance : activer , démarrer et arrêter l’analyse, afficher la transcription.

Le schéma nous montre les différentes étapes nécessaires :

sytody-screen

  1. Notre application Flutter demande l’activation ou la permission d’utiliser la reconnaissances vocale. Si c’est le 1er lancement, sur iOS et Android 7.1+, l’utilisateur doit accepter la demande.

  2. une fois la demande acceptée, l’hôte, appelle invoque une méthode côté Flutter pour confirmer l’activation de la reconnaissance pour l’application demandeuse.

  3. A partir de là, Flutter peut lancer l’analyse en invoquant la méthode “listen” via le canal dédié.

  4. Une fois l’écoute lancée, l’hôte invoque une méthode onRecognitionStarted()

  5. Dès lors, sur iOS, l’application recevra

    • les transcriptions intermédiaires (sujettes à corrections/améliorations),
    • puis, une fois que l’utilisateur arrête la reconnaissance ( speech.stop() ), la transcription finalisée ( sur mon device Android 6.0, l’application reçoit seulement la transcription finale ).

1ère implémentation

Création du projet

flutter create -i swift --org mon.domaine projet_speech
  • -i swift : on souhaite utiliser Swift pour le code iOS, et pas ObjC défini par défaut
  • -a kotlin : si on souhaite utiliser Kotlin à la place du Java par défaut côté Android
  • --org mon.domaine : namespace du projet
  • projet_speech : le nom du projet

Flutter / Dart

On peut créer une classe SpeechRecognizer pour gérer les échanges Flutter/OS

const MethodChannel _speech_channel =
    const MethodChannel("bz.rxla.flutter/recorder");

class SpeechRecognizer {
  
  // gestion des appels effctués par l'hôte
  // dans cette 1ère implémentation on définit un handler globale
  // dans le plugin final dans la 3ème partie
  static void setMethodCallHandler(handler) {
    _speech_channel.setMethodCallHandler(handler);
  }

  // activation / permission
  static Future activate() =>_speech_channel.invokeMethod("activate");

  // démarrage de l'écoute
  static Future start(String lang) =>_speech_channel.invokeMethod("start", lang);

  // arrêt de l'écoute et annulation de la transcription
  static Future cancel() =>_speech_channel.invokeMethod("cancel");

  // arrêt de l'écoute et finalisation de la transcription
  static Future stop() =>_speech_channel.invokeMethod("stop");
}

Dans cette première implémentation, une méthode handler globale est défini pour les appels venant de l’OS

Future _platformCallHandler(MethodCall call) async {
    switch (call.method) {
      case "onSpeechAvailability":
        setState(() => isListening = call.arguments);
        break;
      case "onSpeech":
        if (todos.isNotEmpty) 
          if (transcription == todos.last.label) 
            return;
        setState(() => transcription = call.arguments);
        break;
      case "onRecognitionStarted":
        setState(() => isListening = true);
        break;
      case "onRecognitionComplete":
        setState(() {
          // on ios user can have correct partial recognition
          // => if user add it before complete recognition just clear the transcription
          if (call.arguments == todos.last?.label)
              transcription = '';
          else
            transcription = call.arguments;
        });
        break;
      default:
        print('Unknowm method ${call.method} ');
    }
  }

iOS / Swift

Côté iOS, c’est dans l’appDelegate qu’on créé ici le canal recorderChannel:FlutterMethodChannel, et qu’on définit les handlers pour les différentes méthodes appelées.

override func application(
     _ application: UIApplication,
     didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController

    recorderChannel = FlutterMethodChannel.init(name: "bz.rxla.flutter/recorder",
       binaryMessenger: controller)
    recorderChannel!.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if ("start" == call.method) {
        self.startRecognition(lang: call.arguments as! String, result: result)
      } else if ("stop" == call.method) {
        self.stopRecognition(result: result)
      } else if ("cancel" == call.method) {
        self.cancelRecognition(result: result)
      } else if ("activate" == call.method) {
        self.activateRecognition(result: result)
      } else {
        result(FlutterMethodNotImplemented)
      }
    })
    return true
  }

l’implémentation de ces méthodes ne concerne pas Flutter, donc je ne rentre pas plus dans le détails. cf. AppDelegate.swift

Android / Java

même API côté Android, même si l’implémentation suit ici les principes d’Android.

speechChannel = new MethodChannel(getFlutterView(), SPEECH_CHANNEL);
        speechChannel.setMethodCallHandler(
                new MethodChannel.MethodCallHandler() {
                    @Override
                    public void onMethodCall(MethodCall call, MethodChannel.Result result) {
                        if (call.method.equals("activate")) {
                            result.success(true);
                        } else if (call.method.equals("start")) {
                            recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, getLocale(call.arguments.toString()));
                            cancelled = false;
                            speech.startListening(recognizerIntent);
                            result.success(true);
                        } else if (call.method.equals("cancel")) {
                            speech.stopListening();
                            cancelled = true;
                            result.success(true);
                        } else if (call.method.equals("stop")) {
                            speech.stopListening();
                            cancelled = false;
                            result.success(true);
                        } else {
                            result.notImplemented();
                        }
                    }
                }
        );

cf. MainActivity.java

Voilà pour une v0.1, et pour cette deuxième partie.

Dans la troisième et dernière partie, nous verrons comment modulariser ces fonctionnalités crossplatform, en créant un plugin dédié, facilement réutilisable.

> Flutter, API natives et plugins (3/3)

bistro

Ressources