iOS Преобразование голоcа в текст на Swift 3

В 2016 году на WWDC Apple представила Speech framework, полезный API для распознавания речи. Speech является основой, которая используется Siri для распознавания речи. Если посмотреть повнимательней, то можно найти много фреймворков распознавания речи, доступных на сегодняшний день, но они либо очень дорого стоят, либо отвратительно работают. В этом уроке мы создадим iOS приложение  для преобразования речи в текст с помощью Speech.

Создание интерфейса в Xcode

Давайте начнем с создания нового проекта iOS -> Single View Application. Укажите имя для проекта SpeechToText. Откройте Main.storyboard и перетащите из библиотеки элементов UILabel, UITextView и UIButton. Добавьте текст, расставьте элементы и задайте относительное положение элементов с помощью констрейнтов.

У меня получилось вот так:

Далее следует добавить аутлеты и экшкны для UITextView и UIButton в класс контроллера.

Подключаем Speech фреймворк

Чтобы использовать SpeechKit мы должны сначала импортировать его и сделать основной контроллер соответствующим протоколу  SFSpeechRecognizerDelegate. Данный протокол имеет один опциональный метод, поэтому при его принятии контроллером компилятор не выдаст ошибок. После всех манипуляций ViewController будет выглядеть так:

//
// proSwift.ru
//

import UIKit
import Speech

class ViewController: UIViewController, SFSpeechRecognizerDelegate {
    
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var recordButton: UIButton!
    
    @IBAction func recordButtonTapped(_ sender: UIButton) {
        
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Запрос… всех разрешений

Прежде чем использовать распознание речи и Speech Kit мы должны запросить у пользователя разрешение. Это потому, что непосредственно процесс распознания происходит не на iOS устройстве, а на серверах Apple. А это означает, что данные будут переданы через интернет для обработки и только потом вернутся на устройство. Поэтому запрос разрешения необходим. А также, удовлетворяя параноидальной стратегии Apple, нужно запросить разрешение на использование микрофона.

Запросим разрешение на распознание речи в методе viewDidLoad(). Но для начала добавим в файл info.plist сроку с текстом для запроса. Нажмите на + в последней строке, в колонке ключа найдите пункт Privacy — Speech Recognition Usage Description, и в колонку значений введите строку с вопросом.

Также добавим переменную для экземпляра распознаватель речи.

//
// proSwift.ru
//

 let speechRecognizer = SFSpeechRecognizer(locale: Locale.init(identifier: "ru"))

После измените метод viewDidLoad()

//
// proSwift.ru
//

    override func viewDidLoad() {
        super.viewDidLoad()
        
        recordButton.isEnabled = false // 1
        
        speechRecognizer?.delegate = self // 2
        
        SFSpeechRecognizer.requestAuthorization { // 3
            status in
            
            var buttonState = false // 4
            
            switch status { // 5
            case .authorized:
                buttonState = true
                print("Разрешение получено")
            case .denied:
                buttonState = false
                print("Пользователь не дал разрешения на использование распознавания речи")
            case .notDetermined:
                buttonState = false
                print("Распознавание речи еще не разрешено пользователем")
            case .restricted:
                buttonState = false
                print("Распознавание речи не поддерживается на этом устройстве")
            }
            
            DispatchQueue.main.async { // 6
                self.recordButton.isEnabled = buttonState // 7
            }
        }
    }

Разберем по пунктам:

  1. Пока не известен статус разрешения на распознавание речи, сделаем кнопку записи голосовавшим недоступной.
  2. Для ранее созданного экземпляра speechRecognizer установим делегата. Им будет наш контроллер, который принимает соответствие нужному протоколу.
  3. Вызовем метод класса для запроса разрешения. Этот метод в качестве аргумента принимает функцию, которую вызывает после всех манипуляций с разрешениями, и уже в этом функцию передает статус разрешения. Этот статус мы будем рассматривать в п.5
  4.  Создадим переменную, в которой будем передавать нужное состоянии кнопки в зависимости от результата разрешений
  5. А вот результатов может быть 4. Нас интересует первый вариант. Все варианты результатов, думаю, понятны. Их вариации описаны в комментариях.
  6. Получение и обработка результатов запроса занимает некоторое время. Чтобы не затормаживать пользовательский интерфейс, вся процедура запроса и его обработке происходит в фоновом потоке. А как мы помним из предыдущих статей, изменение интерфейса можно производить только в основном потоке. Поэтому мы получили основной поток  и…
  7. присвоили кнопке записи голоса статус, вычисленный из вариантов полученных разрешений.

Запустите проект и вы увидите мысли разработчиков Apple:

Микрофон

И еще одно разрешение от пользователя. На этот раз микрофона. В файл info.plist добавьте сроку, в колонке ключа найдите пункт Privacy — Microphone Usage Description, и в колонку значений введите строку с нужным вопросом.

Еще раз повторюсь — это текст вопроса, который увидит пользователь при запросе разрешения к микрофону. Но мы еще не пытались получить к нему доступ, поэтому то и вопроса не будет… Но мы к нему подготовились.




Обработка распознавания речи

Мы реализовали авторизацию пользователя, запросы и обработку всех разрешений. Теперь перейдем к реализации непосредственного распознавания речи. Добавим переменные класса ViewController, которым присвоим нужные нам объекты.

//
// proSwift.ru
//

    var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    var recognitionTask: SFSpeechRecognitionTask?
    let audioEngene = AVAudioEngine()Далее, давайте создадим новую функцию startRecording().

Обработка процесса

 

В момент создания задачи распознавания мы должны убедится, что сам процесс вообще возможен. И если по каким либо причинам процесс невозможен, то нужно сделать недоступной кнопку записи. И для этого мы в качестве делегата распознавателя указали наш ViewController. А это означает, что распознаватель будет пытаться вызвать методы делегата, определенные протоколом SFSpeechRecognizerDelegate. Так давайте добавим метод делегата. Я, как и всегда, предпочитаю оформить его в отдельно расширении класса. После декларации класса контроллера добавьте следующие строки

//
// proSwift.ru
//

extension ViewController: SFSpeechRecognizerDelegate {
    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
        if available {
            recordButton.isEnabled = true
        } else {
            recordButton.isEnabled = false
        }
    }
}

КОМПИЛЯТОР ВЫДАСТ ОШИБКУ! Он скажет что класс контроллера уже соответсвует этому протоку. Это действительно так. Не забудьте убрать соответствие протоколу SFSpeechRecognizerDelegate в строке объявления класса ViewController.

Думаю, все понятно. При изменении статуса доступности распознавателя вызывается этот метод и в зависимости от этого статус меняется доступность кнопки начала записи.

Ну и самое последнее, что мы должны сделать, это написать метод, который будет выполнятся по нажатию кнопки записи.

//
// proSwift.ru
//

    @IBAction func recordButtonTapped(_ sender: UIButton) {
        if audioEngene.isRunning {
            audioEngene.stop()
            recognitionRequest?.endAudio()
            recordButton.isEnabled = false
            recordButton.setTitle("Начать запись", for: .normal)
        } else {
            startRecording()
            recordButton.setTitle("Остановить запись", for: .normal)
        }
    }

Внимательно посмотрите на этот метод и самое главное вспомните, в какие моменты происходит управление доступностью кнопки записи. Мы проверяем запущен ли аудио-движок, т.е. пишем ли мы с микрофона и если да, то останавливаем движок, говорим распознавателю, что аудио поток завершился, выключаем кнопку и меняем на ней заголовок. Иначе же, мы запускаем наш метод startRecording() и меняем надпить на кнопке. При этом в этой ветке кода мы не меняем доступность кнопки — она доступна и может остановить запись в любой момент.

Можно тестировать приложение. Запустите на реальном iOS устройстве и нажмите на кнопку начала записи.

При первом запуске будет еще запрос на доступ к микрофону. Мы же его запрограммировали, но еще не давали.

Ну и поговорите с телефоном. Он вас услышит и все запишет.

Что еще нужно знать о Speech в iOS 10

  1. Процесс распознавания речи потребует много энергии и увеличивает расход трафика
  2. Распознавание может длится не более одной минуты за раз
  3. Apple ограничивает количество распознаваний с одного устройства
  4. Apple ограничивает количество распознаваний с оного приложения
  5. Если вы достигните лимита (который не известен), то Apple рекомендует связаться со службой поддержки для увеличения этих лимитов.

Резюме

Мы создали iOS приложение с возможностью распознавания речи. Это приложение использует фреймворк Speech, любезно предоставленный нам компанией Apple.

Привожу полный код ViewController:

//
// proSwift.ru
//

import UIKit
import Speech

class ViewController: UIViewController {
    
    let speechRecognizer = SFSpeechRecognizer(locale: Locale.init(identifier: "ru"))
    
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var recordButton: UIButton!
    
    
    var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    var recognitionTask: SFSpeechRecognitionTask?
    let audioEngene = AVAudioEngine()
    
    
    
    
    @IBAction func recordButtonTapped(_ sender: UIButton) {
        if audioEngene.isRunning {
            audioEngene.stop()
            recognitionRequest?.endAudio()
            recordButton.isEnabled = false
            recordButton.setTitle("Начать запись", for: .normal)
        } else {
            startRecording()
            recordButton.setTitle("Остановить запись", for: .normal)
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        recordButton.isEnabled = false // 1
        
        speechRecognizer?.delegate = self // 2
        
        SFSpeechRecognizer.requestAuthorization { // 3
            status in
            
            var buttonState = false // 4
            
            switch status { // 5
            case .authorized:
                buttonState = true
                print("Разрешение получено")
            case .denied:
                buttonState = false
                print("Пользователь не дал разрешения на использование распознавания речи")
            case .notDetermined:
                buttonState = false
                print("Распознавание речи еще не разрешено пользователем")
            case .restricted:
                buttonState = false
                print("Распознавание речи не поддерживается на этом устройстве")
            }
            
            DispatchQueue.main.async { // 6
                self.recordButton.isEnabled = buttonState // 7
            }
        }
    }
    
    func startRecording() {
        
        if recognitionTask != nil {  // 1
            recognitionTask?.cancel()
            recognitionTask = nil
        }
        
        let audioSession = AVAudioSession.sharedInstance() // 2
        
        do { // 3
            try audioSession.setCategory(AVAudioSessionCategoryRecord)
            try audioSession.setMode(AVAudioSessionModeMeasurement)
            try audioSession.setActive(true, with: .notifyOthersOnDeactivation)
        } catch {
            print("Не удалось настроить аудиосессию")
        }
        
        recognitionRequest = SFSpeechAudioBufferRecognitionRequest() // 4
        
        guard let inputNode = audioEngene.inputNode else { // 5
            fatalError("Аудио движок не имеет входного узла")
        }
        
        guard let recognitionRequest = recognitionRequest else { // 6
            fatalError("Не могу создать экземпляр запроса")
        }
        
        recognitionRequest.shouldReportPartialResults = true // 7
        
        recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest) { // 8
            result, error in
            
            var isFinal = false // 9
            
            if result != nil { // 10
                self.textField.text = result?.bestTranscription.formattedString
                isFinal = (result?.isFinal)!
            }
            
            if error != nil || isFinal { // 11
                self.audioEngene.stop()
                inputNode.removeTap(onBus: 0)
                
                self.recognitionRequest = nil
                self.recognitionTask = nil
                
                self.recordButton.isEnabled = true
            }
            
        }
        
        
        let format = inputNode.outputFormat(forBus: 0) // 12
        
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { // 13
            buffer, _ in
            self.recognitionRequest?.append(buffer)
        }
        
        audioEngene.prepare() //14
        
        do {  // 15
            try audioEngene.start()
        } catch {
            print("Не удается стартонуть движок")
        }
        
        textField.text = "Помедленнее... Я записую...." // 16
    }

}

extension ViewController: SFSpeechRecognizerDelegate {
    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
        if available {
            recordButton.isEnabled = true
        } else {
            recordButton.isEnabled = false
        }
    }
}

Ссылка проекта на GitHub



Добавить комментарий

Ваш адрес email не будет опубликован.

*

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.